The Four Phases
This is the core of the feature. The QA pipeline is a sequence of discrete phases you dispatch by hand — prepare, test, retest, fix — with a human deciding when to move from one to the next.
Business view
Think of it like a QA lead working with a very fast, very literal junior tester:
- prepare — The junior reads the change, writes up a review, and drafts a long list of test cases. You get a tidy epic in Jira with one ticket per test case, and a readable page in Confluence.
- You review the list. You cross out the silly ones, sharpen the vague ones, add the ones they missed. Your edited list is the plan.
- test — The junior runs exactly the tickets that are on the board right now, clicking through the live app, and writes the result — pass or fail, with screenshots — onto each ticket.
- You look at the board. You leave comments: "this fail is actually expected," "retry this on mobile width," "this one's a real bug, fix it."
- retest — The junior re-runs anything still open and anything you commented on, treating your comments as instructions. It never changes the code.
- fix — For the failures you've confirmed are real bugs, the junior edits the code on the branch, redeploys, retests, and closes the ticket with a note about which commit fixed it.
The point: the machine never decides on its own that QA is "done." A person gates every transition. The phases are separate buttons precisely so a human sits between them.
Why phases instead of one button
The workflow the old version offered a full-auto option that ran review → plan → test → fix in a single unattended run. This PR removes that. Every phase is now dispatched deliberately. That is a values choice: for a system that can edit and push code to a branch, "pause and let a human look" is a feature, not a limitation.
Technical view
Where the phases are declared
The four phases are the allowed values of the single action dispatch input:
inputs:
action:
description: QA phase to run
type: choice
required: true
default: prepare
options:
- prepare
- test
- retest
- fix
.github/workflows/sandbox-qa.yml:33-42. Note default: prepare (:37) — the first thing you'd ever run. The old workflow defaulted to test and offered review, plan, test, fix, full-auto plus a separate scope input; all of that is gone (see What Changed).
How a phase is executed
Every phase runs the same single shell command; the phase name is just the third argument:
- name: Run QA agent (${{ inputs.action }})
id: qa
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
set -e
/opt/homelab/sandbox/bin/sandbox-qa dentolize "$GITHUB_REF_NAME" "${{ inputs.action }}" >> "$GITHUB_OUTPUT"
.github/workflows/sandbox-qa.yml:58-64. So the branch under review is $GITHUB_REF_NAME (the workflow is dispatched on a branch ref, not a PR event), and the phase is passed straight through to the sandbox-qa binary.
Honesty note. The per-phase behavior — the Confluence write, the Jira Story creation, the Playwright runs, the PASS→Done transitions, the Bug tickets — is entirely inside/opt/homelab/sandbox/bin/sandbox-qaand theqa-agent/atlassian-qa.pyit calls. Neither file is in this repository. They are homelab infrastructure. From the workflow's perspective, a phase is an opaque command that writesKEY=VALUElines to$GITHUB_OUTPUT. What this repo can prove a phase does is only what those outputs are used for (below); everything else in this page is the documented contract, verified against the header comment (:1-26) and the PR description, not against runnable code in/work/repo.
What the workflow does with a phase's results
After the agent step, the workflow reads these outputs (all produced by sandbox-qa, defensively — each is guarded):
| Output | Read at | Used for |
|---|---|---|
QA_STATE_DIR | :67, :117 | Gate for the comment + artifact steps (only run if the agent produced a state dir). |
JIRA_EPIC_URL | :76 | Linked in the PR summary comment. |
CONFLUENCE_URL | :77 | Linked in the PR summary comment. |
QA_SUMMARY | :78-80 | A file path; its contents are appended to the comment. |
QA_SLUG | :120 | Sanitized branch slug for the artifact name (branch names contain /). |
prepare vs. the rest is the only place the workflow itself branches on the phase name — to choose the summary comment's call-to-action line:
action === "prepare"
? "Stories are ready for the QA team's review in Jira. Delete, refine or add stories on the epic, then dispatch test."
: "Results are on the Jira tickets (comments, screenshots, statuses). Open items = tickets not in Done."
.github/workflows/sandbox-qa.yml:98-100.
Phase-by-phase contract (from the header comment)
The authoritative human-readable spec is the file header, .github/workflows/sandbox-qa.yml:1-26:
- prepare (
:7-11) — "deep code+business review + exhaustive user stories → Confluence page[PR #N] <title> — Quality Review+ Jira Epic[PR #N Quality Review]with one Story per test case." Then the gate: edit the epic natively before dispatching test. - test (
:12-16) — "Playwright-tests the epic's CURRENT Stories on the branch sandbox; per ticket: result comment + screenshots attached + transition (PASS→Done, FAIL→In Progress + labelfailed); Bug tickets for confirmed non-story findings." Gate: review the board, comment on tickets. - retest (
:17-18) — "re-tests non-Done tickets and any ticket with new human comments (treated as instructions). Never commits." - fix (
:19-20) — "fixes confirmed FAIL tickets on the PR branch ([skip ci], self-redeploys, retests); FIXED→Done with the commit ref."
The one phase that touches code: fix
fix is the reason the job requests contents: write:
permissions:
contents: write # fix action pushes to the PR branch
pull-requests: write
issues: write
.github/workflows/sandbox-qa.yml:48-51. The [skip ci] in fix commits is what stops an infinite loop: a normal push to the PR branch would trigger sandbox.yml's deploy, but [skip ci] suppresses that, and the fix phase redeploys the sandbox itself through the sandbox CLI — because the self-hosted runner has a single slot and can't run a deploy while the QA job is holding it. This constraint is documented in the header (:22-26) and mirrors the same self-redeploy pattern used across the sandbox workflows.
Concurrency & timeout
concurrency:
group: sandbox-qa-${{ github.ref_name }}
cancel-in-progress: false
.github/workflows/sandbox-qa.yml:44-46 — one QA phase per branch at a time, and a running phase is never cancelled by a newer dispatch (cancel-in-progress: false). A phase can take a long time: timeout-minutes: 300 (:56). It runs on the homelab runner: runs-on: [self-hosted, homelab, dentolize] (:55).
See also
- The Human QA Gates — the pause-and-steer mechanic in detail.
- Workflow Internals — the summary comment and artifact steps line by line.
- For Quality — what to test and where the edges are.