Delivery¶
The Delivery stage of the product process (DPPD) is where an agreed Plan becomes shipped, operated software. This page is the canonical description of how Delivery runs day to day:
- the Jira statuses a piece of work moves through, what each means, and who moves it,
- the automations that keep Jira in sync with GitHub and the deploy pipeline,
- how the Delivery backlog is organised and prioritised.
It ties together the mechanics documented elsewhere on this site: PR reviews, Branching & releases, and Epic (feature) branches.
One ticket per change
No code lands without a PPL ticket in the PPL project on percihealth.atlassian.net. The ticket key drives the branch name and PR title (see PR reviews) and links the work back to its rationale. Most tickets originate from the Delivery stage of DPPD: a Plan becomes epics and tickets that then move through the statuses below.
Source of truth: live Jira workflow
The statuses and transitions on this page were mapped directly from the live PPL Jira workflow on 19 June 2026. Jira is the source of truth; if the workflow changes, update this page in the same change (see the tip at the bottom of the Engineering index).
Work item types¶
The PPL project uses this issue hierarchy. The Frontend/Backend split is what lets an epic land the backend first and the frontend after it (see Epic branches).
| Level | Types |
|---|---|
| Epic (L1) | Epic |
| Standard (L0) | Story, Task, Bug, Spike, Frontend, Backend |
| Sub-task (L-1) | Sub-task, Design, Frontend Subtask, Backend Subtask |
Status flow¶
flowchart TD
start((Start)) -->|Create| open["Open<br/>(To Do)"]
planning["Ready for Planning<br/>(To Do)"] -->|Ready for work| open
open -->|Back to planning| planning
open -->|Start work| wip["Working on it"]
wip -->|Ready for code review| review["Ready for review"]
review -->|Changes requested| wip
review -->|Ready to test| rft["Ready for Testing"]
rft -->|Start testing| intest["in Test"]
intest -->|Test passed| merge["Ready to Merge"]
intest -->|Bugs found| wip
merge -->|"Merge to Staging ⭐"| staging["On Staging"]
staging -->|Regression passed| release["Ready for Release"]
release -->|Released to prod| done["Done"]
wip -->|No testing needed| done
blocked["Blocked"] -->|Work unblocked| wip
blocked -->|Test unblocked| intest
classDef terminal fill:#d4edda,stroke:#28a745,color:#000;
classDef todo fill:#e2e3e5,stroke:#6c757d,color:#000;
class done terminal;
class planning,open todo;
⭐ = automated transition (see Automations).
Five statuses are global, reachable from any status (the Any badge in Jira): Open, Ready for Testing, On Staging, Blocked and Closed. New tickets are created in Open. Blocked leaves again via Work unblocked (to Working on it) or Test unblocked (to in Test).
Statuses¶
Each status maps to one of Jira's three categories: To Do (blue-grey), In Progress (yellow) and Done (green).
Ownership confirmed with the team
The Moved on by column is who actually performs each forward transition, confirmed with the team on 19 Jun 2026.
| Status | Category | Meaning | Moved on by |
|---|---|---|---|
| Ready for Planning | To Do | A ticket that needs (more) planning before it can be picked up. New tickets are created in Open; Back to planning sends one here and Ready for work returns it to Open. | Product (Ready for work back to Open) |
| Open | To Do | The Delivery backlog: created and ready to be picked up. | Engineer or Designer (Start work; depends on the ticket type) |
| Working on it | In Progress | Being implemented. The engineer (or designer) creates a branch in the relevant project, e.g. PPL-121/change-auth. |
Assignee |
| Ready for review | In Progress | A PR is raised and ready for code review by another engineer. | Engineer, once review passes |
| Ready for Testing | In Progress | Review has passed; the change is handed to QA. | QA |
| in Test | In Progress | QA is actively testing the change on the feature-branch preview (see Testing). | QA: Test passed to Ready to Merge, or Bugs found back to Working on it |
| Ready to Merge | In Progress | Tested and approved. The ticket waits here for the engineer to merge the PR into develop; the merge then auto-moves it to On Staging (see Automations). |
Engineer (merges the PR) |
| On Staging | In Progress | Merged to develop. A release branch is cut and QA runs regression against it. |
QA, once regression passes |
| Ready for Release | In Progress | Regression passed on the release branch; waiting to be released to production. | Release manager (Celina) |
| Done | Done | Released to production. | n/a (terminal) |
| Blocked | In Progress | Work cannot proceed. Reachable from any status; returns via Work unblocked (to Working on it) or Test unblocked (to in Test). | Anyone |
| Closed | Done | Terminal state for cancelled / won't-do work (distinct from Done); not used much in practice. | Anyone |
CONFIRM: status casing
Two status names carry inconsistent casing in Jira: Ready for review (lower-case
"review") and in Test (lower-case "in"). They are written here exactly as
configured. Worth tidying in Jira for consistency; flag if you want that done.
Transitions¶
The workflow uses named transitions (the buttons you click in Jira). The transition name differs from the destination status, and the names are what automations hook onto, so they are listed explicitly here.
| Transition | From → To | What it means |
|---|---|---|
| Create | (start) → Open | A new ticket is created directly in Open. |
| Ready for work | Ready for Planning → Open | Planning done; ready to be picked up. |
| Back to planning | Open → Ready for Planning | The ticket needs (more) grooming before work starts. |
| Start work | Open → Working on it | Engineer (or designer) begins implementation. |
| Ready for code review | Working on it → Ready for review | PR raised, ready for review. |
| Changes requested | Ready for review → Working on it | Reviewer asked for changes; back to the author. |
| Ready to test | Ready for review → Ready for Testing | Review passed; handed to QA. |
| Start testing | Ready for Testing → in Test | QA begins testing. |
| Test passed | in Test → Ready to Merge | QA testing passed. |
| Bugs found | in Test → Working on it | QA found bugs; back to the author. |
| Merge to Staging | Ready to Merge → On Staging | The engineer merges the PR into develop; the automation performs this transition (see Automations). |
| Regression passed | On Staging → Ready for Release | Regression on the release branch passed; queued for release. |
| Released to prod | Ready for Release → Done | Change released to production. |
| No testing needed | Working on it → Done | Fast-path for non-code / admin tickets only (docs, ops). Code must not skip review / merge / release. |
| Work unblocked | Blocked → Working on it | Unblocked; resume implementation. |
| Test unblocked | Blocked → in Test | Unblocked; resume testing. |
| Open / Ready for Testing / On Staging / Blocked / Closed | (global) → that status | Global transitions, reachable from any status (the Any badge). Closed is the cancelled / won't-do terminal. |
Merging into develop is automated
A ticket sits in Ready to Merge until the engineer merges its PR into develop. That
merge triggers the automation that moves it to On Staging and stamps the fix
version (see Automations). Releases are cut from develop, see
Branching & releases.
Notable paths¶
- Fast-path (
No testing needed). For non-code / admin tickets only (docs, ops, config that does not ship as a code release) a ticket may go straight from Working on it to Done. Code changes must not use it:Donemeans released to production, so code still goes through review, merge and release even when QA adds little. - Review rework (
Changes requested). A PR that needs changes goes from Ready for review back to Working on it, so review and rework iterate without losing the ticket's place. - Test rework (
Bugs found). If QA finds bugs, in Test goes back to Working on it rather than forward. - Re-planning (
Back to planning). A ticket in Open that needs more grooming goes back to Ready for Planning; Ready for work returns it to Open.
Definition of Done¶
A code ticket is Done only when the change is released to production and:
- the PR is reviewed and approved by another engineer (see PR reviews);
- comprehensive tests ship with the change: unit / widget coverage and an automated
end-to-end test that captures the user story (the flow that proves the feature works), so it
joins the regression suite. The CI gate must be green (coverage non-regression,
analyzeclean); - it was verified in the feature-branch preview during in Test, and regression passed on staging;
- the acceptance criteria are met;
- any documented behaviour or process it changes is updated in the same PR (treat a missing doc update like a missing test);
- the ticket carries the fix version of the release it shipped in.
Non-code / admin tickets (the No testing needed fast-path) are Done when the work itself is
complete; the code-specific items above do not apply.
Automations¶
Delivery transitions are kept in sync with GitHub by two engines: the Automation for Jira flow below (Jira's native automation, scoped to the Perci Platform project), and a set of GitHub Actions in this repo that call the Jira REST API (see GitHub Actions flows). Documented flows:
Pull request merged to develop -> On Staging¶
| Field | Value |
|---|---|
| Trigger | Pull request merged |
| Conditions | {{pullRequest.destinationBranch}} equals develop, and {{pullRequest.sourceBranch}} contains {{issue.key}} |
| Actions | 1. Transition the work item to On Staging. 2. Edit work item fields: set Fix versions to Next. |
| Owner / actor | Luke Dixon / Automation for Jira |
| On error | E-mail the owner once when the flow starts failing after success. |
When the PR for a ticket (its source branch contains the ticket key) is merged into
develop, the ticket moves from Ready to Merge to On Staging and its fix version
is set automatically. Merging the PR is the engineer's action; the status change and the fix
version are handled for you. (The underlying On Staging transition is global, so it also
applies if the ticket was in another status when the PR merged.)
Fix version lifecycle¶
The automation always stamps the placeholder version Next, never a real version
number. The release itself is cut by a GitHub workflow that takes the release number
manually; that workflow operates only on GitHub (it creates the release branch from
develop) and does not touch Jira. Re-stamping every Next ticket to the actual
release version (for example 1.2.3) is therefore a manual step in Jira at release time.
That re-stamp is what ties each shipped change to the release it went out in. See
Branching & releases.
GitHub Actions flows¶
The six flows below are implemented as GitHub Actions in this repo (rather than as
Automation for Jira rules), so the sync logic lives beside the code, is unit-tested, and is
reviewed like any other change. Each workflow reads the PPL ticket key from the branch name
(or PR title) and calls the Jira REST API through the shared tooling in scripts/jira/.
Every transition is guarded on the ticket's current status, so a rule never drags a
ticket backwards and re-delivered events are idempotent.
| # | Automation | Trigger | Workflow |
|---|---|---|---|
| 1 | Re-stamp fix version at release cut (Next -> X.Y.Z) |
release/* branch created |
jira-restamp-fix-version.yml |
| 2 | Released to prod -> Done | release PR merged to main |
jira-sync-prod-release.yml |
| 3 | PR opened / ready -> Ready for review | pull_request opened / ready_for_review |
jira-sync-pr.yml |
| 4 | Review approved -> Ready for Testing | pull_request_review submitted |
jira-sync-pr.yml |
| 5 | Changes requested -> Working on it | pull_request_review submitted |
jira-sync-pr.yml |
| 6 | Start work on first push (+ assign) | push to PPL-*/** |
jira-sync-push.yml |
The merged-to-develop -> On Staging flow above stays a native Automation for Jira rule;
it is not duplicated here.
PR status mirror (the reverse view). So the pipeline position is visible from GitHub as well as Jira, each open PR whose branch links to a ticket gets two things reflecting its ticket:
- a label with the current Jira status — for example
jira: Ready for revieworjira: in Test— coloured by Jira category (To Do / In Progress / Done); and - a sticky comment with the ticket link, title, status, type, assignee, priority and fix version, updated in place (found by a hidden marker) rather than re-posted.
Both are driven by one Jira read per PR. They are applied on every pull-request event (a step in
jira-sync-pr.yml) and, because the QA-owned stages (in Test, Ready to Merge,
Ready for Release) move the ticket in Jira with no matching GitHub event, a scheduled sweep
(jira-pr-status-sweep.yml, every 30 min) re-syncs all open PRs so those stages show up too.
Only the jira: * label and the marked comment are touched; hand-applied labels and other
comments are left alone. This is the lightweight complement to the Jira Development panel
(branches / PRs / commits on the ticket) that the GitHub-for-Jira app already provides in the
other direction.
Setup
The workflows authenticate with three repository secrets — JIRA_BASE_URL (e.g.
https://percihealth.atlassian.net), JIRA_USER_EMAIL, and JIRA_API_TOKEN (an Atlassian
API token for a bot account). Until all three are set the workflows are a clean no-op
(they log and exit 0), so they can be merged before the secrets exist. Set the repository
variable JIRA_SYNC_DRY_RUN to true to log intended transitions without writing to
Jira — the recommended setting for the first roll-out. Assignment in flow 6 is best-effort:
it resolves the pusher's commit e-mail to a Jira account and silently leaves the assignee
unchanged if the identities are not linked.
The specs below are the behavioural reference the workflows implement (still valid if a flow is ever reworked as an Automation for Jira rule instead). Guard on the current issue status so a rule never drags a ticket backwards.
Specs¶
Each is an Automation for Jira rule (same engine as the live flow). {{...}} are smart
values; always guard on the current issue status so a rule never drags a ticket backwards.
1. Re-stamp fix version at release cut
- Trigger: Branch created.
- If: branch name matches
release/(.*); capture the version from{{branch.name.match("release/(.*)")}}. - Then: for each issue in JQL
project = PPL AND fixVersion = "Next", create the version if needed and set Fix versions to the captured version. - Alternative (preferred): do it in the cut-release GitHub workflow, which already knows
X.Y.Z, by calling the Jira REST API. Pick one, not both.
2. Released to prod -> Done
- Trigger: Pull request merged.
- If:
{{pullRequest.destinationBranch}}equalsmainand{{pullRequest.sourceBranch}}matchesrelease/(.*)(also allowhotfix/*); capture the version. - Then: for each issue in JQL
project = PPL AND fixVersion = "<captured>" AND status = "Ready for Release", run the Released to prod transition to Done.
3. PR opened -> Ready for review
- Trigger: Pull request created.
- If:
{{pullRequest.sourceBranch}}contains{{issue.key}}, the PR is not a draft, and the issue status is Working on it. - Then: run the Ready for code review transition to Ready for review.
- Note: PRs may target the epic branch, not
develop, so do not condition on the destination branch here.
4. Review approved -> Ready for Testing
- Trigger: Pull request reviewed, review state = approved.
- If:
{{pullRequest.sourceBranch}}contains{{issue.key}}and the issue status is Ready for review. - Then: run the Ready to test transition to Ready for Testing.
5. Changes requested -> Working on it
- Trigger: Pull request reviewed, review state = changes requested.
- If:
{{pullRequest.sourceBranch}}contains{{issue.key}}and the issue status is Ready for review. - Then: run the Changes requested transition to Working on it.
6. Start work on first push (+ assign)
- Trigger: Commit created (or Branch created).
- If: the commit message / branch name contains
{{issue.key}}and the issue status is Open. - Then: run the Start work transition to Working on it and set Assignee to the commit author (needs linked GitHub / Jira identities).
Feasibility
All six run today as the GitHub Actions in GitHub Actions flows,
which call the Jira REST API directly — so the review-state triggers (3-5) do not depend on
Automation for Jira exposing the pull request reviewed event, and the release-side rules
(1-2) key off the release/* branch name (created at cut, merged to main at release).
Backlog¶
The backlog is a single ranked, groomed queue, not an archive. You finish a ticket, go to the backlog, and take the next one off the top. We run it Kanban-style: continuous pull, no sprints.
What lives where¶
- Ready for Planning (the Kanban backlog): still being groomed, off the active board.
- Open (the To Do column): groomed and ready to pull; the top is next.
- Closed: stale or won't-do; a terminal state in the Done / Released column.
A ticket is ready (belongs in Open) when it has a clear description, an Epic, a Component, a Priority, and any blockers linked. Otherwise it stays in Ready for Planning.
Order is priority¶
The ready queue is rank-ordered (drag order): the top of To Do (Open) is what to
pick next, and the Priority field drives that order. Don't scan the list and cherry-pick;
take the highest-ranked ticket you are eligible for.
Area: the Component field¶
Every ticket carries a Component marking the part of the platform it touches. Because the backend and all the frontends share one board, the Component is what makes "which ticket is for me?" obvious. The components are:
| Component | Area | Mostly picked up by |
|---|---|---|
| Member App | The perci-platform-members Flutter app |
Frontend |
| Clinical App | The perci-platform-clinicians Flutter app |
Frontend |
| Website | The brochure site (percihealth.com) |
Frontend |
| Backend | Functions, BFFs, Medplum bots, and the web-professionals portal |
Backend |
| Infrastructure | CI/CD, deploys and platform plumbing | Backend / platform |
| Admin | Cross-cutting admin and ops (e.g. documentation) | Any |
Component is used rather than the issue type because it applies to every issue type: a
Bug or Spike belongs to an area just as much as a Story does. Board quick filters
(one per component, plus Mine and Unassigned) and Epic swimlanes key off Component, so
"what is the next Backend (or Member App) ticket I can pick up?" is one click.
Admin is the catch-all
Admin covers cross-cutting and internal work that is not tied to a specific app (for
example documentation), so anyone may pick it up. The web-professionals React portal is
treated as Backend.
The pick rule¶
When you finish a ticket, take the next one in this order:
- Stay on your epic. The top-ranked ready ticket of your component(s) in the epic you are currently working. This keeps you in context.
- Otherwise move epic. If your component(s) have nothing ready in that epic, take the top-ranked ready ticket of your component(s) in the highest-ranked epic that still needs it.
- Rank breaks ties.
Epics are not formally assigned to people; this is a soft "prefer your current epic" rule, to cut context switching without locking anyone to an epic.
Frontend / backend ordering within an epic¶
Backend usually lands before the app work that depends on it (see Epic branches). So within an epic:
- Rank the Backend ticket above the app ticket (Member App / Clinical App, etc.) that needs it.
- Link the app ticket is blocked by the Backend ticket. A blocked ticket is not ready, so it will not be picked until its backend dependency is done.
Board setup¶
The PPL board is a Kanban board with the backlog enabled. Configure it under Board settings so the queue and the areas are obvious:
- Backlog on. The Kanban backlog is enabled and holds
Ready for Planning(grooming);Opensits in the To Do column as the ready-to-pull queue, rank-ordered (top is next). - Quick filters (one click to your work):
Member App:component = "Member App"Clinical App:component = "Clinical App"Website:component = WebsiteBackend:component = BackendInfrastructure:component = InfrastructureAdmin:component = AdminFrontend:component in ("Member App", "Clinical App", Website)Backend / platform:component in (Backend, Infrastructure)Mine:assignee = currentUser();Unassigned:assignee is EMPTY
- Swimlanes by Epic, so each epic is a row. With a component quick filter you see "the next Member App ticket in epic X" at a glance.
- Card layout and colours. Show Components, Priority and Epic on the card, and colour cards by Component so the areas pop.
- Columns (one per status, as on the Scrumban board): Kanban backlog =
Ready for Planning; To Do =Open; In Progress =Working on it; Blocked =Blocked; Ready for Review =Ready for review; Ready to Test =Ready for Testing; In Test / Review =in Test; Ready to Merge =Ready to Merge; On Staging =On Staging; Ready for Release =Ready for Release; Done / Released =Done+Closed.
Keeping the backlog healthy¶
- One-time clean-up. Triage the current pile: Closed for stale or irrelevant, Ready for Planning for the rest, then add Component / Epic / Priority and rank whatever survives into Open.
- Weekly refinement. Product and the leads keep the top couple of weeks of the backlog groomed and ranked; below that can stay rough.
- Stale guard. A saved filter flags stale
Opentickets for re-triage so the queue does not silt up again:project = PPL AND status = Open AND updated <= -90d ORDER BY updated ASC(optionally a Jira automation comments on or labels them).
Relation to DPPD¶
The backlog is fed by the Plan stage of DPPD: a Plan becomes an Epic plus its tickets, which enter the backlog (in Ready for Planning until groomed, then Open). Prioritising across epics is a Product / Plan decision; the backlog rank reflects it.
Proposed: leaner status model¶
Proposal, not yet applied
The statuses above are the live workflow (the source of truth). This section is a proposed improvement to discuss; nothing here is in Jira yet.
Why. Six in-progress queue states after coding (Ready for review -> Ready for Testing ->
in Test -> Ready to Merge -> On Staging -> Ready for Release) inflate cycle time, each is
a manual drag, and two names are inconsistently cased.
Proposed active states: Ready for Planning, Open, In Progress, In Review, In QA, On Staging, Ready for Release, Done (plus Blocked, Closed).
| Current | Proposed | Change |
|---|---|---|
Working on it |
In Progress | rename: matches the board column, drops the colloquial name |
Ready for review |
In Review | rename + fix casing |
Ready for Testing + in Test |
In QA | merge two queue states into one (keep both only if the QA queue is genuinely long, which is itself worth fixing) |
Ready to Merge |
(removed) | enable PR auto-merge on approved + green instead of a manual hand-off state |
On Staging |
On Staging |
keep (or rename In Regression for an action-style name) |
Net 12 -> ~9 statuses: fewer manual moves and honest cycle-time. If you keep the current
shape, at minimum fix the casing (Ready for Review, In Test) and rename
Working on it -> In Progress, then update the automations and board columns to match.
Related¶
- Product process (DPPD): how an idea becomes Delivery-ready work.
- PR reviews: branch/PR naming, review and merge rules.
- Branching & releases: the
develop->release->mainflow. - Epic (feature) branches: long-lived feature branches per epic.