Merging is the deploy. There is no ds deploy and no pulumi up in the normal flow: deploys are triggered by pushing to a tracked branch and orchestrated by Seed, not by GitHub Actions. The one exception is ds seed deploy, which asks Seed to redeploy a chosen commit to a stage with the team's shared token. It can deploy production, so it is never run against prod without the owner's yes.
develop or master; ds seed deploy is only for a deliberate redeploy of a chosen commit. What you run locally (ds run backend) provisions your own personal SST stage for local development — see the callout below — it does not touch the shared dev/prod environments.The repo's seed.yml defines the build hooks that Seed runs on every push to a tracked branch. Stripped of the toolchain pinning (Node, Corepack, the pnpm store path), the hooks that matter are:
before_build:
- bash ds -ru script generate-api-docs
before_deploy:
- bash ds -bru script set-config
- bash ds -ru script delete-orphan-stacks
- bash ds -ru script check-orphan-stacks
- bash ds -ru migrations run-sql-expand
after_deploy:
- bash ds -ru script sync-identity-providers
- bash ds -ru migrations run-sql-contract
before_build regenerates the published OpenAPI spec so it matches the routes being deployed.before_deploy pushes the environment's config into the SST DEVSTRIDE_CONFIG secret (-b forces a fresh bind, so generate-once secrets keep the value currently deployed rather than a cached snapshot), deletes and then guards against orphaned CloudFormation stacks left behind by stacks removed from sst.config.ts, and applies the expansion migrations — additive schema changes both the old and the new Lambda revisions can live with during the rolling multi-stack deploy.after_deploy runs as a separate Seed build process on a cold cache. It first restores Google sign-in on the Cognito app client (CDK resets the provider list on every deploy; see sync-identity-providers), then applies the contract migrations — the destructive or tightening changes that are safe only once every new Lambda is live. The order is deliberate: a contract migration that fails (a lock timeout on a busy table) stops the process and rolls back to run again on the next deploy, and must never leave Google sign-in switched off with it.This split is why migrations must be idempotent and rolling-deploy safe: for a few minutes the old code runs against the expanded schema, and an event emitted by the old revision can be consumed by the new one. Locally, ds migrations run applies both chains at once. None of this happens on your machine — it is the exact sequence Seed executes on every deploy.
It's easy to assume .github/workflows/ is where deploys live, since that's the conventional place for them. In this repo, it isn't. All ten workflow files run checks or maintenance jobs — none of them deploy anything. Most run on pull requests; the expensive ones do real work only when a change is on its way to production:
| Workflow | Trigger | What it does |
|---|---|---|
backend-tests.yaml | Pull requests; pushes to develop and master | Runs the sharded backend vitest suite, but only where it adds coverage — see below |
backend-tests-manual.yaml | Manual dispatch only | Runs the full backend suite on demand against any pushed branch — every shard, no filters. It calls backend-tests.yaml rather than repeating it, so its results appear under Backend Tests (manual) / … and can never stand in for a pull request's own verdict — see below |
ci-policy.yaml | Pull requests | Enforces the draft-first convention: a pull request a person opens straight to ready fails this check, with the convention explained |
lint-backend.yaml | Pull requests into master; pushes to master | Type-checks the backend (pnpm run check:ts), then runs the infrastructure synthesis tests and the dead-letter-queue wiring checks |
lint-frontend.yaml | Pull requests into master; pushes to master | Type-checks the frontend (pnpm run check:ts), then runs eslint, errors only (pnpm run lint:errors) — the backstop for lint errors the local runs missed. Warnings never fail it |
ui-tests.yaml | Pull requests into master; pushes to master | Runs the frontend test suite |
merge-path.yaml | Pull requests into develop, drafts included | Posts the required merge-path check: only an epic integration branch, the support train or its release snapshot, a master-into-develop sync, a release, hotfix or Dependabot branch may merge into develop, unless the pull request carries merge-path-override. It reads its rules and checker from develop itself, so a pull request cannot edit its own gate. ds merge-path check gives the same verdict locally — see Command Reference |
mcp-tests.yaml | Pull requests that touch the MCP server (packages/mcp) or its install inputs; manual dispatch | Type-checks and tests the DevStride MCP server. Not a required check |
golden-republish.yaml | Pushes to master; manual dispatch | Reconciles the canonical golden dataset source after each release — migrate, re-certify, a few minutes on the standard runner. A full rebuild is local-only: the workflow opens a golden-refresh-failure issue for the operator when one is needed |
claude.yml | Issue/PR comment containing @claude | The Claude Code PR-comment bot |
lint-backend, lint-frontend and ui-tests do their work only on a pull request into master — the release pull request from a release/YY-MM-DD branch, or a hotfix — and on the push to master that lands it. A push to develop runs none of them, and on a pull request into develop or an epic branch their jobs report skipped, which still satisfies the required checks, so the merge is not blocked. In the cloud, a lint, type or frontend-test break is therefore found on the release pull request, the same way a backend-suite break is — but every pull request into develop has already run the same checks locally, as the required local-ci check.
The push to master that merges a release does not repeat them: it skips only when it can find a successful run of the same job on the commit being promoted, so a direct push or a hotfix to master is still checked.
One part of lint-backend cannot wait for the release: SEED deploys develop on every merge, so a stack that names a missing handler or queue would break the shared dev deploy long before a release pull request saw it. The infrastructure synthesis tests and dead-letter-queue checks therefore also run locally, as the infra-synthesis pre-ship check, on any pull request that touches stacks/** or deploy configuration.
develop posts a skipped lint and UI check on its head commit. If the same branch is also open against master, that skipped result sits on the same commit and can hide a failing master-bound check. The backend workflow mirrors its verdict across the two; these three do not.local-ci: the required check on developBecause the cloud runs so little on a pull request into develop, the deep checks run on the team's own machines instead. Every pull request into develop must carry a green local-ci commit status before it can merge. It has been a required check on develop since 2026-10-04, alongside merge-path.
local-ci is posted by ds verify full --post, which runs the full local mirror of cloud CI on the pull request's exact head: a frozen-lockfile install, every type-check, eslint over the frontend (errors only), the MCP and frontend tests, the UI build and portal budget, a readiness check on the test database, the whole non-golden backend suite, and the pre-ship checks the change triggers. It takes about half an hour, and it posts success only when every step passed on that commit with a clean tree. The delivery loop runs it before it marks a pull request ready.
Every kind of pull request into develop can get one:
--adopt-release). The status names the run it adopted.dependabot/batch-<YY-MM-DD> branch by ds work dependabot-batch and checked once, rather than once per update.Below it, every Story and one-off has already passed ds verify land before it merged onto its Epic's branch or the train: every type-check, eslint on the changed frontend files, and every test whose imports reach the changed code.
Why. Before, a pull request into develop ran almost nothing in the cloud, so a break in the linters, the UI tests or the backend suite was first found on release day, with the whole release waiting on it. Running the same checks locally, on machines the team already has, finds it before the merge and costs nothing in CI minutes. The release pull request into master keeps its cloud run as the last line of defence.
The backend suite is by far the most expensive thing in CI, so it runs only where it adds coverage:
master (from the release/YY-MM-DD branch cut from develop) — once per release.master that merges it, the suite is not repeated. Each run records the exact code it tested, and the push reuses the release pull request's green result when that record matches what is being merged. Anything missing, unreadable or different, and the suite runs.master whose code was not already tested green.run-backend-ci label, when it is opened, reopened, edited or marked ready — not on later pushes. To prove new commits, convert the pull request back to draft and mark it ready again.develop only when the repository variable BACKEND_TESTS_ON_DEVELOP_PUSH is true. It is off by default, so an ordinary merge into develop runs no shards.gh workflow run backend-tests-manual.yaml --ref <branch>
ds wrapper around it: the one-line command above is the whole feature. GitHub only offers workflows that exist on the branch you pick, so a branch cut before this workflow landed has to be rebased on develop first.Everywhere else the required Backend Tests check reports skipped, and the reason is printed in the "Detect backend changes" job beside it, which is green. A required check is satisfied by a successful, skipped or neutral result, so a skipped check still merges — what it no longer does is look identical to a run that tested something.
So the check now tells you which happened:
master, or the release pull request's green run reused by the push that merges it. The reason is printed either way.The trade-off is deliberate. The cloud would find a merge that breaks the suite only when the release is cut, but the whole suite has already run locally on every pull request into develop, as part of the required local-ci check, and every Story ran the tests that reach its changes before it merged. When you want the cloud suite over a branch sooner than that, the manual run above is the way to get it.
Backend Tests (manual) / … and never the required Backend Tests context.Deploy workflow files did exist here once, but they were deleted years ago — they're simply gone, not present-but-defunct. Today, GitHub Actions is a checks-and-maintenance pipeline — pull-request gates, plus the comment bot and the golden dataset reconcile that follows each release push — while the deploy pipeline lives entirely in Seed's own infrastructure.
By convention:
develop → deploys the dev stage, served at app.devstride.devmaster → deploys the prod stage, served at app.devstride.comdevelop, hotfix branches PR into master, and the dev/prod UI domains line up accordingly), not something you can grep for. If you need to confirm or change it, that's a Seed dashboard change, not a code change.The frontend isn't deployed by a separate pipeline. It ships as part of the same SST deploy, via the sst.StaticSite construct (stacks/ui/app-ui.stack.ts) — built with pnpm run build:ui and pushed to S3/CloudFront in the same sst deploy that ships the backend Lambdas.
That stack is gated off when you're running locally: sst.config.ts only registers the SiteStack when !app.local, so ds run backend (which runs pnpm exec sst dev) never tries to build or deploy the frontend — for local frontend work you run ds run ui separately, which just points a local Vite dev server at your bound stage's API.
There are exactly two documented cases for invoking the SST CLI directly instead of going through CI. The first is scoped to the packages/mcp package specifically:
DEVSTRIDE_STAGE=prod pnpm exec sst deploy
packages/mcp/README.md documents this to ship the MCP server, noting explicitly that it's part of the same SST deployment with no separate pipeline of its own — this is a raw sst invocation, not wrapped by any ds command. Treat it as an advanced, narrow escape hatch for that package, not a general-purpose way to deploy the app. Similarly, the root README.md documents raw sst remove for tearing down your own personal stage — again a direct SST CLI call, not a ds subcommand.
Outside of these two narrow, explicitly-documented cases, the only supported path to a deployed dev or prod environment is: merge to develop or master and let Seed do the rest.
ds run backend / ds run uids command surface