Dentolize · Jira/Confluence QA Pipeline Walkthrough
On this pageBusiness viewTechnical viewSee also

Workflow Internals

This page walks the actual file — .github/workflows/sandbox-qa.yml, the only file this PR changes — top to bottom. If you're reviewing the diff, read this alongside What Changed.


Business view

The workflow file is small and does three jobs:

  1. Run one QA phase. It calls one command on the homelab and hands it the branch name and the phase you picked.
  2. Update the PR comment. It writes (or rewrites) a single comment on the pull request: a header, a timestamp, links to the Jira epic and Confluence page, a one-line "what to do next," and whatever summary the agent produced.
  3. Save the evidence. It uploads the agent's working directory as a downloadable artifact so nothing is lost if you want to inspect a run later.

Everything clever — the reviewing, the testing, the Jira and Confluence writing — happens outside this file, on the homelab. The file's own job is plumbing: dispatch, comment, archive.


Technical view

1. Trigger and inputs

on:
  workflow_dispatch:
    inputs:
      action:
        description: QA phase to run
        type: choice
        required: true
        default: prepare
        options: [prepare, test, retest, fix]

.github/workflows/sandbox-qa.yml:30-42. Manual onlyworkflow_dispatch. You go to the Actions tab, pick "Sandbox QA," pick the branch you want QA'd, and pick a phase. There is no push/pull_request trigger, so nothing here runs automatically.

2. Concurrency, permissions, runner

concurrency:
  group: sandbox-qa-${{ github.ref_name }}
  cancel-in-progress: false

permissions:
  contents: write        # fix action pushes to the PR branch
  pull-requests: write
  issues: write

.github/workflows/sandbox-qa.yml:44-51.

  • One phase per branch, never cancelled mid-run (:44-46).
  • contents: write exists only for fix. prepare/test/retest never need it — a point worth remembering when reasoning about least privilege (see For Quality).
  • issues: write looks surprising now that "GitHub story-issues are retired" (PR body). It's still needed because a PR comment is an issue comment in GitHub's API — the summary-comment step below calls github.rest.issues.listComments / createComment / updateComment (:104-114).
  • Runner: runs-on: [self-hosted, homelab, dentolize] (:55), timeout-minutes: 300 (:56).

3. The agent step

- 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.

  • One command. set -e means a non-zero exit fails the job (and the header, :22-24, calls out that "job success means results are published" — the run is foreground, no detached agent).
  • The binary appends KEY=VALUE lines to $GITHUB_OUTPUT. Those become steps.qa.outputs.* for the later steps. This is the entire contract between the homelab agent and this workflow.
  • Only GITHUB_TOKEN is passed in. The old workflow also passed QA_SCOPE, QA_REVIEW_FILE, QA_PLAN_FILE (all removed).

4. The PR summary comment

- name: Update PR summary comment
  if: always() && steps.qa.outputs.QA_STATE_DIR
  uses: actions/github-script@v7

.github/workflows/sandbox-qa.yml:66-114.

Guard: always() && steps.qa.outputs.QA_STATE_DIR (:67) — run even if the agent step "failed," but only if the agent got far enough to emit a state dir.

The step:

  1. Reads JIRA_EPIC_URL, CONFLUENCE_URL, and the QA_SUMMARY file (:76-80).
  2. Finds the open PR for this branch by head ref (:82-85); if there's none, it logs "no open PR — nothing to comment" and returns (:86). So dispatching QA on a branch with no open PR does the QA work but posts nothing.
  3. Builds the comment body (:88-102) — a hidden marker <!-- sandbox-qa-report -->, an ### 🧪 Quality Review heading, a timestamp + phase + run link, the two Atlassian links (each only if present, :95-96), the phase-specific call-to-action (:98-100), and the agent's summary text.
  4. Upserts by marker: find an existing comment containing the marker, update it if present, otherwise create it (:104-114). So the PR accumulates exactly one QA comment that gets rewritten each phase.

Full comment builder:

const marker = "<!-- sandbox-qa-report -->";
const lines = [
  marker,
  "### 🧪 Quality Review",
  "",
  `_${now} · phase **${action}** · [run](${runUrl})_`,
  "",
  ...(epicUrl ? [`**Jira epic:** ${epicUrl} _(the QA workspace — edit stories there)_`] : []),
  ...(pageUrl ? [`**Confluence:** ${pageUrl} _(review + living test report)_`] : []),
  "",
  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.",
  "", summary,
];

.github/workflows/sandbox-qa.yml:88-102.

Honesty note — this is the whole Atlassian surface in the diff. The only things this workflow does with Jira and Confluence are: print JIRA_EPIC_URL and CONFLUENCE_URL as links. It never calls a Jira or Confluence API. Every Story, transition, screenshot attachment, results table, and Bug ticket described in the PR is produced by the off-repo qa-agent/atlassian-qa.py. When reviewing this diff, you are reviewing a dispatch-and-comment wrapper, not the Atlassian integration itself.

5. Artifact upload

- name: Upload run artifacts
  if: always() && steps.qa.outputs.QA_STATE_DIR
  uses: actions/upload-artifact@v4
  with:
    name: sandbox-qa-${{ steps.qa.outputs.QA_SLUG }}-${{ github.run_id }}
    path: ${{ steps.qa.outputs.QA_STATE_DIR }}
    if-no-files-found: warn
    retention-days: 14

.github/workflows/sandbox-qa.yml:116-123. The agent's state directory is archived for 14 days. QA_SLUG is used because raw branch names can contain /, which is illegal in an artifact name.

The output contract, collected

The sandbox-qa binary is expected to emit some or all of:

KeyPurpose
QA_STATE_DIRGates both post-steps; also the artifact path.
QA_SLUGFilesystem-safe branch slug for the artifact name.
JIRA_EPIC_URLOptional link in the comment.
CONFLUENCE_URLOptional link in the comment.
QA_SUMMARYPath to a file whose contents go in the comment.

Every consumer is null-guarded, so a phase that emits only QA_STATE_DIR still produces a valid (if link-less) comment.


See also