Skip to content

Deployment Workflows

Wavemap separates validation, application delivery, operator work, documentation publication, and infrastructure mutation. The separation is a safety property: each lane answers a different question and receives only the authority needed to answer it.

LaneTriggerQuestionMutation Boundary
Continuous integrationPull request, develop push, or manual dispatch.Is this source revision healthy?Repository and disposable test services only.
Application-only deliveryManual dispatch with an exact merged SHA.Can these immutable app images safely replace the Development runtime?App images, modeled runtime deploy, release receipt, and bounded rollback.
Deployed-dev operatorManual dispatch with one reviewed profile.Which explicit Development operation or recovery proof should run?Profile-dependent; destructive and lifecycle actions remain visibly selected.
Docs publicationManual dispatch with a selected ref.Can the curated static site publish and smoke?Docs bucket and CDN invalidation only.
Infrastructure changeGuarded local operator command.Is a reviewed resource-graph transition safe to apply?Exact saved full-stack plan only.

No push or CI-completion event dispatches either application or docs CD. A green merged-state CI run makes an exact commit eligible for manual application delivery; it does not deploy that commit.

.github/workflows/ci.yml runs on pull requests targeting develop or main, pushes to develop, and manual dispatch. Pull-request CI tests the proposed merge. Push CI records the health of the exact merged develop SHA that downstream application delivery must find before it can obtain package or cloud authority.

The workflow keeps separate jobs for workflow contracts, change classification, tooling/code, docs, source i18n, rendered pseudolocalization, ordinary hermetic tests, database-backed tests, browser smoke, Docker builds, and pull-request base freshness. Repository-owned classification skips only the expensive docs, rendered-pseudolocalization, and Docker lanes when the changed paths cannot affect them; uncertain paths receive full assurance.

See Testing Runtime And CI for the exact job graph, local commands, and source trail.

.github/workflows/deploy-application-dev.yml has one trigger: workflow_dispatch. The operator supplies a full lowercase 40-character Git SHA and confirms that the run is application-only.

Source Admission Before Delivery Authority

Section titled “Source Admission Before Delivery Authority”

The first eligibility job validates the full SHA, proves it is contained by develop, and requires successful push-event ci.yml for that exact merged SHA before checking out source or obtaining package/cloud credentials. PR checks and cancelled merged-state runs do not satisfy that gate.

A second job uses the separate dev-application-admission environment and metadata-only OIDC role. It reads the current sanitized deployment contract, canonical release receipt and exact SERVER_VERSION parameter. The receipt must identify the modeled target, repositories, activation posture and full-SHA version. Missing, inconsistent, unavailable or divergent baselines require reviewed operator bootstrap; admission never falls back to the candidate’s last commit.

The typed classifier examines the complete deployed-to-candidate Git range. Migrations, schema/database configuration, public-reader policy and privileged maintenance changes require operator delivery, including when a later app-only commit follows them. After acquiring host ownership, delivery rereads the contract/receipt/version and checks the admitted baseline hash before mutation. A concurrent baseline change therefore requires fresh admission.

Private npm Supply Chain owns the package token’s exact CI and Docker boundary; it grants no cloud or deployment authority.

After admission, the dev-application-cd environment grants the dedicated delivery role. The workflow:

  1. Acquires host mutation ownership and rechecks the admitted baseline.
  2. Reads the three exact restricted-public frontend build parameters, masking their values.
  3. Builds or reuses backend, frontend and, when modeled, public API images tagged sha-<full-git-sha>, then resolves every selected image to a registry digest.
  4. Sends the complete immutable bundle to the modeled runtime document. The host writes the selected SERVER_VERSION before rendering environments, waits for container health, and checks running image/version identity.
  5. Runs endpoint and non-destructive browser-routing smoke, then records the verified release on the host and publishes the same receipt to the canonical history/current objects.
  6. On a post-deploy failure, redeploys the preserved previous receipt, republishes that restored receipt and reruns endpoint smoke. Rollback restores the old version before environment rendering.
  7. Reopens and releases ownership only after a recorded successful release or a verified rollback. Cancellation, failed verification or an uncertain command retains ownership for operator recovery.

The delivery role can publish only to the modeled application repositories, read the sanitized contract and release metadata, read the three frontend build parameters and exact version parameter, write release receipts, invoke the modeled document on the modeled instance, and invoke the exact private lifecycle maintenance alias. It cannot read reader credentials, write SSM parameters, run arbitrary shell commands, perform database maintenance, mutate media, update Pulumi or directly control EC2. The host owns the exact SERVER_VERSION write.

Application rollback changes images and their release identity. It does not reverse schema, infrastructure, configuration, reader grants or credential rotation. A release with database-affecting changes belongs to the reviewed operator lane.

Both workflows use wavemap-dev-host-mutation with cancellation disabled. A persistent owner token on the host spans deploy, migration/reset, smoke, recording and rollback; a Linux flock serializes each host command and prevents release while a command is active. This protects repository entrypoints across jobs and manual calls; it does not constrain an administrator’s arbitrary root or AWS access.

Manual operations use deploy dev runtime ownership and retain the same WAVEMAP_DEPLOY_MUTATION_TOKEN through the entire operation. Tokens start with a letter or digit and contain 16–128 letters, digits, underscores or hyphens. These commands default to a local plan; add --execute only for the separately approved live operation:

Terminal window
export WAVEMAP_DEPLOY_MUTATION_TOKEN="reviewed-operation-unique-id"
pnpm wavemap -- deploy dev runtime ownership --action acquire --pulumi-outputs-json /private/path/deployment-contract.json
# Execute the approved acquire, then the approved deploy/maintenance/record or rollback commands with this same token.
pnpm wavemap -- deploy dev runtime ownership --action release --pulumi-outputs-json /private/path/deployment-contract.json

Acquisition first closes automatic wake/stop in the private lifecycle coordinator, resumes the host through that same private capability, then acquires host ownership and removes Caddy’s persistent open marker. The API receives SIGTERM and drains its existing pool before database maintenance. A deployment may start the candidate reader behind the closed proxy; migrate/reset stops it again before the retained-role preflight. When the API is enabled, database commands require --public-reader-role or WAVEMAP_PUBLIC_READER_ROLE; the operator workflow reads that non-secret role name from its protected environment variable. Regrant and --check run while readers remain stopped. Ordinary application delivery gains no database administrator authority.

Release verifies frontend and backend readiness, starts the retained public service when present, performs a real rich Event read and zero-row private-data denial checks using its reader credentials, and verifies that no table-level mutation grants are present. It then reopens the cloud lifecycle, repeats host verification and publishes the open marker under the original host lock. A crash between cloud reopening and marker publication can leave automatic wake enabled while public reads remain closed; recovery restores cloud closure before proceeding. These two stores are not one transaction. Already-fresh CloudFront responses may remain visible for their existing maximum 60 seconds; urgent removal requires separately approved invalidation.

There is no lease timeout or automatic takeover. After interruption, confirm that the old workflow and all SSM commands have ended, inspect running images/version and release receipts, then use --action status and --action recover with the original token. Recovery resumes only that owner or finishes acquisition that never reached the host; it cannot replace another owner. --action retry-transition explicitly repeats only a recorded uncertain start/stop, without deleting its intent; --action reconcile-dns publishes the running host’s freshly observed address. These actions retain the same dry-plan/--execute boundary. Do not delete the owner, lock, lifecycle record or admission marker to bypass recovery.

The operator workflow completes its selected database, browser and media checks before verified release. Wake and cold-start smoke follow release, because testing public wake during maintenance would test an intentional refusal. Refresh the sanitized contract after provisioning the private lifecycle handle. Establish a compatible public-image baseline containing the maintenance verifier before activation; an older rollback image without it remains closed and needs operator recovery.

The recorder writes /opt/wavemap/deployment/last-successful-runtime-release.json only after the modeled host action validates the running release. The wrapper then copies that identical metadata to application-releases/aws-dev/history/<full-sha>.json and application-releases/aws-dev/current.json in the deployment-contract store. Both workflows and manual rollback use this owner. Recording and rollback require confirmed completion; live --no-wait is refused.

Receipt validation and its inferred type live in infra/operations/src/deployed-dev/runtime-release-receipt.ts, shared by admission, rollback and host recording. The modeled SSM document embeds that same portable TypeScript source and runs it through the existing backend container’s Node 24 runtime. This does not require Python or Node installed on the host, nor a newly packaged command in an older rollback image. The validator receives explicit metadata inputs and emits validated JSON; Bash writes a private temporary sibling and renames it to the fixed host receipt only after successful validation. Failed validation or interrupted output preserves the previous receipt. The container has no mount of the host receipt directory.

Host and S3 writes are not one transaction. A failed or uncertain copy can leave the host/history ahead of current, or report failure after current was already updated. Preserve the selected previous receipt before deployment. Under the same ownership, either retry recording the verified running release or explicitly roll back to that preserved receipt and republish it. Verify host/current/version agreement before releasing a manual recovery. Do not fabricate a receipt or edit only SERVER_VERSION to bypass admission. History keys are per source SHA, with store object versioning retaining repeated publications.

The source model is not evidence that live grants exist. Before activating these workflows:

  • Apply the separately reviewed IaC transition: modeled mutation/record document, public repository where selected, exact repository publish/pull policies, admission role and host-only PutParameter for /wavemap/dev/runtime/backend/SERVER_VERSION.
  • Configure the protected dev-application-admission environment with AWS_ADMISSION_ROLE_ARN, DEPLOYMENT_CONTRACT_STORE_BUCKET and the private package read token. Its role trusts only the repository/environment OIDC subject and reads only the two current metadata objects plus the version parameter.
  • Review the externally managed operator role’s trust and exact grants: selected ECR repositories, modeled document/instance and command readback, contract reads, and s3:PutObject for the canonical current/history release keys. Its separate database-maintenance authority is not inherited by application delivery.
  • Ensure the host has Linux flock and Docker Compose supporting up --wait, and the backend image retains the pinned Node 24.15.0 runtime or a verified compatible successor for embedded TypeScript execution. Complete a reviewed operator deployment and publish a verified compatible baseline before ordinary application delivery can be admitted.

Public capability bootstrap and the remaining activation gates are documented in Public API Runtime.

.github/workflows/deploy-dev.yml is also manual-only. It accepts a reviewed ref and one deterministic run_profile, then pins every downstream checkout to the SHA resolved by preflight. It does not accept arbitrary combinations of stage switches.

ProfileIntended useImportant boundary
preflightRepository and deployment-contract checks.No cloud authentication or mutation.
preflight-dockerPreflight plus local image-build proof.Slow; no push or cloud mutation.
cloud-planOIDC, live metadata, and deploy dry runs.Cloud-aware but non-mutating.
deploy-endpointMigration-aware app/API operator deploy.May apply pending migrations; no reset.
deploy-endpoint-recoveryEndpoint deploy plus wake and browser-routing recovery.Non-destructive data posture.
deploy-seeded-browserKnown-data and browser proof.Includes destructive database reset.
deploy-mediaMedia API, storage, CDN, browser, and discrepancy proof.Includes reset and temporary media mutation.
deploy-lifecycleStop, cold-start page, wake, and browser recovery.Includes reset and deliberately stops the host.
deploy-full-validationDeliberate combination of all expensive proofs.Broadest and most disruptive recipe.

Use Runbooks for selection and evidence expectations. The database remains disposable at this project stage, but destructive behavior is still explicit so a human knows what was destroyed and why.

The reviewed operator role used by deploy-dev.yml must separately receive ssm:GetParameters for the same three exact frontend-build parameter ARNs before a live frontend image job can use this lane. That external role permission remains a human-controlled activation step rather than an inferred repository mutation.

The normal code path is intentionally layered:

human selects exact source and operation
-> GitHub workflow owns trigger, environment, permissions, and job order
-> public wavemap CLI owns stable command names
-> shell wrapper owns live process control and --execute gates
-> @wavemap/operations owns typed plans, validation, and summaries
-> Pulumi-owned cloud/runtime contracts constrain provider mutation
OwnerRead here firstOwns
Workflow orchestration.github/workflows/deploy-application-dev.yml, deploy-dev.ymlAdmission, environment, permissions, concurrency, DAG, summaries.
Typed GitHub/cloud handoff.github/actions/prepare-application-delivery-cloud-job, prepare-cloud-deploy-contextAllowlisted environment inputs and credential setup.
Public commandspackages/cli/src/wavemap-cliStable operator routes and help.
Live executionbin/dev-deployProvider CLIs, waits, dry-run/--execute gates, GitHub outputs.
Typed deployment logicinfra/operations/src/deployed-devProfiles, eligibility, plans, bundles, receipts, validation.
Cloud/runtime contractinfra/pulumi/src/providers/awsRoles, repositories, SSM documents, host, storage, control plane.
Regression contractspackages/cli/src/wavemap-cli/__tests__, infra/operations/src/**/__tests__Trigger, ordering, profile, validation, and permission invariants.

When behavior changes, edit the lowest owning layer and keep the workflow declarative. Do not duplicate business logic in YAML or make docs the only place where a safety rule exists.

Application and docs workflows read wavemap.deployment-contract version 1 from the private deployment-contract store. That projection contains deploy-safe facts, not raw Pulumi state or decrypted runtime secrets.

The contract also carries a separate frontendBuildConfiguration projection containing only the prefix, exact parameter paths, and restricted-public kinds. It never carries provider values. Frontend image jobs fetch those values directly from SSM after AWS authentication, mask them, and keep them out of workflow summaries and artifacts.

GitHub Actions
-> reads deployment targets, parameter references, image identity, and deploy metadata
-> sends a modeled SSM command that references parameter paths
runtime host
-> reads /wavemap/dev/runtime/* directly from SSM Parameter Store
-> renders private environment files and Compose locally
-> pulls the selected images and starts containers

Rendered environment files and decrypted SecureString values must never enter workflow logs, summaries, artifacts, or the deployment contract. See Configuration And Secrets for value ownership and Infrastructure Change Policy for the complete store/actor map.

.github/workflows/deploy-docs.yml supports only workflow_dispatch. It checks out the selected ref, obtains the docs role through the dev environment, reads the sanitized contract, builds apps/wavemap-docs/dist, publishes that static directory, invalidates CloudFront, and performs read-only public smoke.

It does not access the application host, API, database, media store, wake path, app release receipt, runtime secrets, or Pulumi state. Working notes are not public input unless their conclusions are deliberately promoted into curated pages. See Docs Hosting for source ownership and permission details.

Application and docs workflows never run pulumi up. Wavemap infrastructure changes use bin/infra/pulumi-stack.sh, a clean exact source checkout, and a refresh-enabled saved full-stack plan. Update authority requires the exact reviewed plan SHA-256 and source Git SHA. Tendril owns the backend foundation; Wavemap owns only its /wavemap product state and workload graph.

See Guarded Pulumi Operations And Recovery before any preview, update, or state recovery.

LaneDefault postureWhat it proves
EndpointApplication delivery and operator deploy.Public entrypoint, backend health/readiness, i18n manifest.
Browser routingApplication delivery and recovery recipes.Non-destructive public rendering and unauthenticated routing.
Database status/migrateReviewed operator path.Live Drizzle ledger compatibility and bounded forward migration.
Database reset/seeded/browserDestructive selected recipes.Known disposable data and seeded journeys.
Media/browser-media/discrepancyMedia recipe.Real object storage/CDN behavior and DB/object consistency.
Wake/cold-startRecovery or lifecycle recipe.Cost-control stop/wake behavior and browser recovery.
DocsEvery intentional docs publication.Public static routes and assets without waking the app.

Higher-cost smoke belongs in the default gate only after it is stable, bounded, diagnosable, and worth its runner and operator cost.

Use pnpm wavemap -- ... for the public command surface and pnpm wm -- ... as the local shortcut. Root scripts remain compatibility entry points for workflows and older notes; the authoritative arguments and mutation posture live in the Wavemap CLI Command Reference.

ChangeAuthoritative home
Public command or flagCLI Command Reference and packages/cli/src/wavemap-cli.
Workflow order, trust, or handoffThis page and .github/workflows.
Manual procedure or recoveryRunbooks and the owning wrapper.
IAM or resource lifecycleInfrastructure Change Policy and Pulumi source.
Environment cost/data postureDeployed Dev and environment configuration.
Static publicationDocs Hosting and docs deploy source.