Developer Guide

Release

How finished work reaches develop and then production: an Epic's release pull request, the support train, /devstride:release, Seed's deploys, the post-deploy check, and how to tell whether a commit is live.

Release

On this page you will follow finished work the rest of the way: from an Epic's branch or the support train into develop, from develop into a production release, and then check that production is running it and is healthy.

Why develop is treated as production

develop is cut into a production release several times a day, and merging a release into master deploys production automatically. Whatever is on develop when the next release is cut goes to customers, usually within hours. There is no quiet period in which to finish or tidy something up.

So nothing merges into develop unless it is complete and safe to go live that afternoon. That is why Stories collect on their Epic's own branch, where they are allowed to be unfinished, and only a finished, fully reviewed Epic merges into develop. Before an Epic merges:

  • Nothing it deploys may point at something missing. A stack that names a handler, file or resource that does not exist fails the deploy and blocks the pipeline for everyone, not just the author.
  • New infrastructure that costs money or changes behaviour is agreed with the owner first (the owner is the person who decides what reaches production). It is never a note someone finds later in production or in a bill.
  • Every change a user can see is safe to be live at once, and you can say who it reaches on day one and what, if anything, gates it.
  • Database migrations are safe while old and new code run side by side, because a deploy rolls out gradually. Deployment explains how Seed (the hosted service that deploys this application) runs them.

How DevStride Is Built, and Why tells the story behind these rules.

The path at a glance

Story    ─ merge ─▶ its Epic's branch ─ Epic release pull request ─▶ develop
One-off  ─ merge ─▶ train/support     ─ train pull request, at each release ─▶ develop
develop  ─ cut ──▶ release/YY-MM-DD   ─ release pull request + owner's yes ─▶ master

Seed deploys every merge into develop to the dev stage (a stage is a separate, fully deployed copy of the application), and every merge into master to production.

Release an Epic into develop

An Epic's Stories merge onto its integration branch (named <you>/<YY-MM-DD>/epic-<I#####>-<slug>, where I##### is the Epic's item number) without pull requests of their own; Develop covers that part. When the last one lands:

  1. The loop reports the Epic release-ready, and stops. In this repository epicIntegrationBranches.autoRelease is false in .claude/ds-config.json, by the owner's decision, so a finished Epic waits until the owner says to release it.
  2. It brings the Epic's branch up to date. It merges the latest develop into the branch: a merge, never a rebase, because the branch's commits are already shared on GitHub.
  3. It opens the Epic's release pull request into develop as a draft, through /devstride:pr. The description leads with the Epic and lists its Stories. While a pull request is a draft, CI (the automated checks GitHub runs) waits, apart from a few cheap policy checks such as merge-path (below).
  4. Everything gets a full review. Claude's adversarial review (one that sets out to find what is wrong) and the Codex command-line reviewer (a second, independent reviewer that runs on the same machine) go over the whole Epic's changes, every Story included. It is the first full review any of this code has had, because each Story merged after only a short risk check (the quick review a Story gets before it merges). Copilot (GitHub's own AI reviewer, billed per review) does not review pull requests into develop; it first sees this code on the production release. Every finding is checked, then fixed or dismissed with a reason, and every review thread is answered and resolved. Two rounds is the normal target; a confirmed serious problem keeps review going until a round finds none.
  5. The full local check posts local-ci. Before the pull request is marked ready, this runs on the machine:
    ds verify full --post --adopt-release
    

    It is the local copy of everything cloud CI runs on a release, and takes about half an hour: an install of exactly the dependency versions in the lockfile, every type-check, the frontend linter (errors only), the tests of the MCP server (packages/mcp, the tool server that lets AI agents work with DevStride) and of the frontend, the UI build and the customer portal's size budget, the whole backend test suite apart from the golden dataset's own tests (the golden dataset is the generated demo data), and any check the changed paths trigger. A change to stacks/** or deploy configuration, for example, also runs the infra-synthesis check, because Seed deploys develop on every merge and a broken stack would break the shared dev deploy. With --post it marks the pull request's head commit (the latest commit on its branch) local-ci pending while it runs, then success or failure. It refuses to start on a tree with uncommitted changes, and if the head commit or the files change during the run it posts no result at all. (--adopt-release only matters for the sync after a release, below.)
  6. Marking the pull request ready starts cloud CI, once, on the reviewed code. On a pull request into develop that is a short list: ci-policy (which checks the draft-first convention), the MCP tests when packages/mcp changed, and the backend's quick gate job. The backend test suite, the linters and the UI tests report skipped here, which still satisfies them; they run on the release into master.
  7. It merges. The Epic's branch is deleted, and the release pull request is linked back onto the Epic and its Stories. Seed deploys develop to the dev stage.

What you should see

On the Epic's release pull request: merge-path green from the moment it opens (it runs on drafts); the pull request a draft until review settles; local-ci green before it is marked ready; then Backend Tests, the linters and the UI tests skipped. develop requires merge-path and local-ci before anything merges.

Which branches may merge into develop: the merge-path check

develop reaches production at the next release, so only branches that have been through a full review may merge into it. The required merge-path check admits these heads, and nothing else:

Head branchWhat it is
<you>/<YY-MM-DD>/epic-<I#####>-<slug>an Epic's integration branch
train/support, or the snapshot a release cuts from itthe support train (below)
a master-into-develop sync branchthe close-out of a release, below
release/…a release branch
<you>/hotfix/…a hotfix branch
dependabot/…a Dependabot dependency update

The rules are mergePath.allowedHeadPatterns in .claude/ds-config.json. The check runs on drafts too, and reads its rules from develop itself, so a pull request cannot loosen its own gate.

Any other head fails unless the pull request carries the merge-path-override label. That is for a change that must ship on its own pull request (deploy configuration or migrations, below) or an Epic branch named before the epic- marker existed. Nothing adds the label for you:

gh pr edit <number> --add-label merge-path-override

To know before you open the pull request, run the same checker locally. It prints ALLOWED or BLOCKED with the reason:

ds merge-path check --head <branch> --base develop
ds merge-path check --head <branch> --base develop --labels merge-path-override

See Command Reference: ds merge-path.

One-offs ride the support train

A one-off is a Story or Defect outside any plan, usually made with /devstride:create-story or /devstride:create-defect: it has no [N] number and no dependency links. Even when it is filed under an Epic, it never goes on that Epic's branch (with ds work start, pass --one-off for one filed under an Epic). It does not get a pull request of its own. After a short risk check and a green ds verify land (the landing check every item passes), it merges onto the support train, the long-lived branch train/support. Then:

  1. It stays open, not Done, with a comment saying it ships with the next release.
  2. At the start of every release, the train goes into develop first. /devstride:release takes a snapshot of the train (the live train keeps accepting one-offs) and opens a pull request from it into develop. That pull request gets the full Claude and Codex review over the whole train, the local-ci check and one CI run: the one-offs' first full review. It merges before the release is cut.
  3. Every one-off it carried is marked Done, with a comment naming the release.
  4. The train is brought up to date with develop straight after its pull request merges, and again when the release closes out. The release does this for you; ds work train rotate does it by hand once a release has carried everything on the train.

To see what is waiting, run ds work train status. It lists the one-offs on the train that have not reached develop yet, and changes nothing.

Deploy configuration and migrations never ride the train. A one-off that changes stacks/, sst.config.ts or the database migrations under backend/src/libs/infrastructure/database/migrations/ would reach production soon after the train merges. Instead it ships to develop by its own pull request, so the dev stage deploys it before any release carries it. The delivery loop routes such a change this way by itself; by hand, start it with ds work start <I#####> --infra and open its pull request with /devstride:pr. Either way, nothing adds the merge-path-override label for you: add it with gh pr edit <number> --add-label merge-path-override. The pull request then gets the full review and local-ci like any other pull request into develop. Develop has the commands in context.

Promote to production: /devstride:release

The owner decides when to release. Running the command prepares a release; it never counts as permission to deploy.

Run it from a leased wt1 or wt2 checkout (see Develop), never the main checkout, and keep the lease until the release's very last step hands the checkout back.

/devstride:release
  1. It reads what is happening first. It fetches, reads what other people and sessions landed on develop and master in the last few hours, and says so if another session seems to be active.
  2. It merges the support train into develop, as above.
  3. It works out what ships: the Epics and items in develop that are not yet in master, and which of them a user would notice.
  4. It cuts the release branch release/YY-MM-DD from develop (with -2, -3 added when a release is re-cut), and opens the release pull request from it into master as a draft. develop keeps accepting merges meanwhile; they ship in the next release.
  5. Full review of the release. Claude and Codex, plus Copilot, which reviews only pull requests into master. Copilot is asked once, on the final reviewed version, and again only after a fix that answered one of its findings.
  6. Release-only local checks. The full golden dataset suite runs, and so does the golden source readiness check, ds golden release check: the release will not go ahead without a prepared, certified copy of the demo dataset for exactly the code being released. The Golden Dataset page explains the golden source.
  7. Cloud CI runs once, when the pull request is marked ready: the backend test suite, the linters and the UI tests. The push to master that lands the release reuses this green run when the code is identical, so the suite does not run twice.
  8. It stops and asks the owner. The summary leads with READY or BLOCKED and lists every change in plain words, each local check's result, the documentation plan, whether a release note will be written, and what merging triggers, quoted from release.autoDeployOnMerge: "Merging to master auto-deploys production via SEED (seed.run) — no manual deploy step."
  9. It merges only on the owner's explicit yes, with a merge commit.
  10. After the deploy, it checks production. Production has no address that reports which version it runs, so the release first asks the owner to confirm the deploy has finished. Then it runs the post-deploy check, below. FAIL stops the release and puts a rollback decision to the owner; nothing public is written about a release that might be rolled back.
  11. It updates the documentation. The repository's ds-update-docs skill brings docs.devstride.com in line with what shipped, by default, only once the deploy is confirmed live and healthy. Add no docs to the command to skip it.
  12. It writes a release note only when asked: /devstride:release --release-notes publishes one after the deploy is confirmed, and --release-notes draft leaves it for the owner to read first. Nothing in the loop decides on its own that a release deserves a note; the owner does.
  13. It closes out. It syncs master back into develop by its own pull request, so fixes made on the release branch reach develop; that pull request's local-ci reuses the release's green cloud run in seconds when the code is identical (--adopt-release). It deletes the release branch once both branches contain it, and brings the support train up to date.

A release branch is protected. Fixes found during review are new commits on it, never a rebase or force-push; GitHub refuses a force-push to any release/* branch. A fix that touches deploy configuration or migrations is refused there: merge it into develop through a normal pull request, abandon the release branch, and run /devstride:release again to re-cut. If a hotfix reaches master while a release is open, the release merges master into its branch and reviews it again, and asks the owner again.

The Delivery Loop: Production release has the full mechanics, and Configuration Reference: Production release has the settings.

How Seed deploys

Seed (seed.run), a hosted deploy service, deploys this application. A merge into develop deploys the dev stage (app.devstride.dev), and a merge into master deploys production, the prod stage (app.devstride.com). GitHub Actions deploy nothing, and there is no deploy step to run by hand: merging is the deploy.

During each deploy Seed runs the additive migrations before the new code goes live and the tightening ones after it. Deployment explains this and the checks that run in GitHub Actions. An ordinary production deploy took about seventeen minutes from start to finish when it was measured.

Is this commit live?

Seed writes a GitHub deployment record for every commit it deploys, under the environment prod or dev, created by its own GitHub account, seed-deploy[bot]. Read those records with gh, the GitHub command-line tool; it needs to be signed in.

To see which commit production is running, run this from the repository's root:

REPO=devstride/devstride bash .github/scripts/resolve-live-production-sha.sh

It prints the full commit id. It trusts only records made by Seed's own account whose newest status is a success, so a record anyone else wrote cannot fool it. It exits with status 1 when it finds no deployment Seed proved successful, and 2 when GitHub's API failed or was rate-limited. In both cases the answer is "unknown", never "not live".

To check that your change is part of what production runs:

git fetch origin master
git merge-base --is-ancestor <your commit> <the commit it printed>

Exit status 0 means production's commit contains yours.

To see the latest deploys of a stage, newest first:

gh api --paginate 'repos/devstride/devstride/deployments?environment=prod&per_page=100' \
  --jq '.[] | select(.creator.login=="seed-deploy[bot]") | [.sha[0:9], .created_at] | @tsv' | head -5

Use environment=dev for the dev stage. Each line is a commit Seed started deploying and when it started. A record on its own does not prove the deploy finished; for production, the script above checks that.

The post-deploy check

After the owner confirms the deploy, /devstride:release runs the repository's post-deploy check, the ds-post-deploy-release-check skill (named in .claude/ds-config.json as release.postDeployCheckSkill). It does three things:

  1. It checks production's health, without changing anything, through the ds-post-deploy-health skill:
    • it confirms from Seed's deployment record that production is running the merge commit, waiting up to 25 minutes for a deploy that is still rolling out;
    • it waits until at least 15 minutes after the rollout finished, so alarms have had time to fire;
    • it looks for any alarm that went off after the deploy started, even one that has since recovered; any dead-letter queue (where messages land after processing fails) that grew compared with just before the deploy; API server errors above the level that trips their alarm; problems on the inbound email path; and any web-app file the deploy meant to upload that is missing from the site's storage bucket.

    Alarms too slow to fire inside that window are listed as not covered, never counted as passed. Problems that were already there before the deploy are listed separately, so they are seen but do not fail the check.
  2. It publishes the golden source (the canonical copy of the demo dataset that training sandboxes and stage refreshes clone from) for the release that is now live, and reads it back to prove it is current.
  3. It removes the local resources the release created, such as its test containers.

Its first line is exactly one of POST-DEPLOY HEALTH: PASS, POST-DEPLOY HEALTH: FAIL or POST-DEPLOY HEALTH: NOT RUN, followed by the evidence, each line naming the command that produced it. PASS lets the release go on to documentation. FAIL stops it for the owner's rollback decision; a failure only in publishing the golden source is repaired and retried instead, and never calls for rolling the application back. NOT RUN means the check could not be done, for example because a sign-in was missing or a service could not be reached, and the owner is asked whether to proceed. Missing credentials are never a PASS.

You can also run the health check by hand after any production deploy: ask Claude Code to run the ds-post-deploy-health skill for the merge commit. It needs a valid AWS sign-in to the production account and a signed-in gh; Credentials and access covers both.

An alarm that fires soon after a deploy is not proof the deploy caused it. Before blaming the release, look at when the alarm's underlying metric first went over its threshold; it may have started before the deploy.

A deliberate redeploy: ds seed deploy

Normal releases never need a deploy command, because merging is what deploys. For a deliberate redeploy of a chosen commit, ds seed deploy asks Seed to deploy it to a stage:

ds seed deploy --stage <stage> --commit <commit id>
ds seed deploy --stage <stage> --commit <commit id> --force

--force deploys even when Seed finds no changes. The command sends the same request as Seed's own command-line tool, for DevStride's Seed organization and app (--org and --app default to devstride). It needs no AWS sign-in. It uses the team's shared Seed token, SEED_TOKEN, from your machine's agent credentials file (~/.config/devstride/agent.env, written by ds secrets pull), and never prints it.

If the token has not been added yet, the command refuses and explains how to add it: the owner makes a token in Seed's organization settings and stores it with pbpaste | ds secrets set agent SEED_TOKEN, and each machine then runs ds secrets pull. Credentials and access has the full map of keys.

Hotfixes

A hotfix is an urgent fix that must reach production without the unreleased work on develop. Run /devstride:branch-hotfix I#####-<short-slug> on a clean tree: it reminds you to stop any running dev server, cuts <you>/hotfix/<YY-MM-DD>/I#####-<short-slug> from a freshly pulled master, pushes it, and resets this checkout's local database so it matches production's older code. Build the fix, then open its pull request into master with /devstride:pr. It gets the draft-first treatment, with Claude, Codex and Copilot reviewing, then CI, including the backend test suite, because this code has not been tested before. Open it only against master, never against develop as well: a branch open against both can hide a failing check. /devstride:pr stops when the pull request is reviewed and green; it does not merge it. Merging into master deploys production, so that merge is the owner's call. If a release is open when the hotfix lands, the release merges it into its own branch and reviews it again. Like everything on master, the hotfix comes back to develop through the master-into-develop sync at the end of each release.

When something goes wrong

  • merge-path is red. Run ds merge-path check --head <branch> --base develop for the reason. A one-off belongs on the train, and only an infrastructure one-off or an old-style Epic branch should carry merge-path-override.
  • local-ci is missing, failed, or stuck at pending. Re-run ds verify full --post on a clean tree at the pull request's head; a run whose head or files changed part-way posts nothing, so its pending mark stays until a later run replaces it. It needs this checkout's test containers, and only one runs per machine at a time. The full log is .ds/verify-full.log.
  • A fix on a release branch is refused. It touches deploy configuration or migrations. Merge it into develop by a normal pull request, abandon the release branch and re-cut.
  • The post-deploy check says FAIL. The owner decides whether to roll back. To triage an alarm without changing anything, paste the alert to the ds-alarm skill; ds-incident-response is the full on-call protocol.
  • The post-deploy check says NOT RUN. Usually a sign-in: the message names the command to run, either an AWS sign-in that reaches the production account (aws sso login --profile <profile>; see Your AWS sign-in has lapsed) or gh auth login. Run it, then run the check again. A deploy still rolling out after 25 minutes is also NOT RUN: check again once it finishes.

Anything else: Troubleshooting.

Next: Troubleshooting, for when a step on any page does not go as described.