Releasing TuneToys app versions
How an app version is built, qualified, promoted and rolled back.
The step-by-step process for promoting a tested immutable Linux container to the AWS pilot, how artifacts are identified, and what rollback can and cannot do. This releases the website/API only; creator Toy publications and Player/plugin builds are different release streams.
Status: Implemented as a manual process — no automatic production deployment, rolling deployment or zero-downtime path exists. Saving source, committing, pushing or running npm run build/check does not update tune.toys. The v0.0.3 pipeline has landed but has not run on GitHub; CI has no AWS identity, so images are published to ECR by the operator, and the host promotes them by digest with promote.sh once the owner installs it.
Rules that never change: keep a retained rollback artifact and confirm a fresh private S3 backup before each promotion; app deployment must never move a customer's pinned Toy version or replace audio in a running DAW. Current production identity and checks are in the AWS pilot record; host setup is in hosting.
Release flow#
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#FAFAF7', 'primaryTextColor': '#1C1C1B', 'primaryBorderColor': '#1C1C1B', 'lineColor': '#64645F', 'fontFamily': 'DM Sans, Inter, system-ui, sans-serif'}}}%%
flowchart LR
DEV["Reviewed source"] --> CHECK{"Checks pass?"}
CHECK -->|Yes| IMAGE[["Frozen Linux image<br/>source + engine receipt"]]
IMAGE --> TEST{"Exact artifact<br/>qualified?"}
TEST -->|Yes| BACKUP[("Fresh S3<br/>recovery archive")]
BACKUP --> LOCK["Deployment lock"]
LOCK --> LIVE(["Restart Lightsail API"])
LIVE --> HEALTH{"HTTPS and<br/>flow checks"}
HEALTH -->|Pass| KEEP["Record release<br/>and back up"]
HEALTH -->|"Fail and compatible"| ROLLBACK["Restore previous image"]
classDef source fill:#F0442E,stroke:#1C1C1B,color:#FAFAF7
classDef shared fill:#DCEBDD,stroke:#1C1C1B,color:#1C1C1B
classDef host fill:#DED8F0,stroke:#1C1C1B,color:#1C1C1B
class IMAGE source
class CHECK,TEST,HEALTH shared
class BACKUP,LOCK,LIVE,KEEP,ROLLBACK host| Step | Action | Evidence to retain |
|---|---|---|
| 1. Review | Choose the exact change; review source, dependencies, migrations, configuration and compatibility | Commit/tag when available, reviewed diff and release notes |
| 2. Check | Run npm run check; run affected HTTP/browser/native checks | Logs tied to the chosen source |
| 3. Build | npm run build:container -- --platform=linux/amd64 | Exact .cache/containers/<input-sha>/<run-id>/build.json, frozen context and image |
| 4. Qualify | node tools/test-container.mjs /absolute/path/to/build.json; inspect the built UI and relevant journeys | Exact receipt/image/engine digests and results |
| 5. Back up | Confirm target account/region/host/current image; run a fresh host backup and inspect its new confirmed S3 result | Archive key/version/checksum, current image/config and recovery reference |
| 6. Stage | Publish to ECR by digest; or (fallback) save the exact image ID, transfer it through SSH, verify and load it | Registry digest and receipts/<inputSha256>.json, or archive SHA-256; matching host image labels/platform |
| 7. Promote | Hold the shared deployment lock, compare the expected old image, atomically replace image.env, restart the API | Previous image.env, new identity and operator/promotion record |
| 8. Verify | Check loopback/public HTTPS readiness, app UI, sign-in, permissions and changed flows | Smoke checks and decision to keep or revert |
| 9. Retain | Confirm a new backup includes the new image, update the deployment record, keep the prior image/archive | Release notes, rollback artifact and post-release backup |
The 2 GB host is not a build machine: build on the development/build host and preserve the shared prepared engine bytes. Use a temporary test environment rather than an always-on staging bill. The current production target is Linux amd64, account 982471468586, Ohio us-east-2, instance tunetoys-pilot, static IP 3.20.14.74. Confirm these against AWS before acting; historical documentation is not target-discovery authority.
Version and artifact identity#
- Use a human app version/changelog for communication and immutable input/image/engine digests for deployment. About reads the version from
apps/web/package.json. - Choose a patch/minor/major app version before building; for example,
npm version patch --workspace @tunetoys/web --no-git-tag-version --ignore-scriptsupdates the web version and lockfile without publishing or creating a Git tag. Review both files. - API, Player and independent product versions have separate compatibility meanings. Never bump engine ABI, source schema or plugin identity as a cosmetic app-version change. A new app version does not rewrite immutable packages, media hashes, stable parameter IDs, plugin IDs, signed exports or saved DAW state; engine ABI/semantic and portable schema changes need their own compatibility/fixture review.
- The repository currently has development package versions; a package version or Git tag alone does not prove which bytes are running. Record the source commit when the checkout has been committed. For the current pilot, frozen source manifests also identify reviewed artifacts produced while other chats edit the checkout.
- A receipt contains
inputSha256,engineSha256,imageId,platformand its frozencontext. Never deploy a floating tag such aslatest. A changed checkout prevents the build tool from advancing its local latest pointer; the retained older artifact is not automatically approved. Inspect it, compare its manifest with the desired changes and qualify that exact receipt, or rebuild from a stable checkout. Check current source/tests separately from the source actually inside a retained image. - The sidebar release is an example of a narrow update built from the prior deployed snapshot while later editor/DSP work continued, preserving the deployed engine/API. Normal releases should use one isolated, reviewed checkout so the final artifact includes exactly the intended changes.
Publish to ECR#
This is the current publish step. It replaces the archive transfer when the host has promote.sh installed; the code-only promotion steps below remain the fallback. The automated path is the same tool: release-web.yml runs it with --ambient-credentials once the tunetoys-ci role exists, which the organization's service control policy currently prevents (see the workflows README).
- Commit the reviewed change, then build and qualify from that clean checkout:
npm run build:container -- --platform=linux/amd64andnode tools/test-container.mjs /absolute/path/build.json --smoke. - Preview:
node tools/publish-image.mjs /absolute/path/build.json --dry-run. It prints every command and any refusal without calling AWS. - Publish:
node tools/publish-image.mjs /absolute/path/build.json. The AWS profile is--profile, elseAWS_PROFILE, elsetunetoys; the repository and bucket are--repository/--bucketorTUNETOYS_ECR_REPOSITORY/TUNETOYS_AUDIT_BUCKET(values in the workflows README). It refuses a dirty checkout, container inputs that differ from the receipt's frozen snapshot, and a local image whose platform or labels differ from the receipt. It logs in to ECR, tags the receipt's image ID<repository>:<inputSha256>and<repository>:sha-<commit>, pushes, printsdigest=sha256:…, and writes the receipt (without its local path, plus registry, commit and caller) once tos3://<audit bucket>/receipts/<inputSha256>.json. Tags are immutable: an existing tag or receipt is accepted only when it names the same image. The profile needs ECR push ontunetoys/apiands3:PutObjectonreceipts/; the host'stunetoys-host-deliverypolicy grants pull only. - On the host, after a fresh confirmed backup is available:
sudo tunetoys-promote promote <imageId> --expect <running imageId> --digest <digest> --input <inputSha256> --engine <engineSha256> --kind image --run <digits> --commit <sha>. It pulls by digest, verifies identity and labels, refuses a migration downgrade, takes the lock, proves the backup, swapsimage.env, restarts, checks health and rolls back an image-only failure. A schema release runstunetoys-promote rehearse … --kind schemafirst, thenpromote … --kind schema --recovery <option>. Then complete steps 8 and 9 of the release flow.
The receipt's imageId is whatever the building Docker daemon reports, and promote.sh compares it with the pulled image's ID. A builder using the containerd image store reports the OCI index digest (the local builds on November 1, 2026 did), while a classic-store daemon reports the image config digest. Confirm that the builder and the host report the same ID for one image before the first promotion from ECR.
Code-only promotion#
Use the trusted administrator SSH identity and pinned known-hosts file. Keep private operator configuration outside Git. Review commands before running them; credentials are not command arguments or release payloads.
- Save the candidate with
docker image saveusing its receipt's exact image ID, compress it and compute SHA-256. Retain the exact receipt alongside it. No.env, local account database or private operator directory belongs in an image. - Transfer through authenticated SSH to a unique temporary staging directory. Verify the archive checksum before loading. Inspect the loaded image and require Linux amd64 plus matching
toys.tune.input-sha256andtoys.tune.engine-sha256labels. Loading an image is staging, not promotion. - Start a fresh backup with
sudo systemctl start tunetoys-backup.service. Inspect the new/var/log/tunetoys/operations.logentry confirming its versioned S3 archive. A successful service exit is insufficient: a run skipped because the deployment lock is held deliberately emits no backup success. Investigate/retry before changing production. - Acquire an exclusive
flockon/srv/tunetoys/operator/backup-work/deployment.lock. Backups and promotions share that inode; never remove/recreate it. Compare/etc/tunetoys/image.envand the running container against the expected previous image. Abort if another operator changed production. - Preserve the previous root-only
image.envand public release metadata under/srv/tunetoys/operator/releases/<input-sha>/. Write onlyTUNETOYS_IMAGE=sha256:<verified image ID>to a mode0600temporary file in the same config directory and atomically rename it overimage.env. Do not source Docker env files as shell code or print the privateapi.env. - Restart with
sudo systemctl restart tunetoys-api. Checkhttp://127.0.0.1:8787/v1/healthandhttps://tune.toys/v1/health, public HTML/asset loading and changed UI behavior. Keep the lock until the promotion/compatible rollback decision finishes. Configuration stays on the encrypted mounted data disk; Caddy/domain/mail usually need no change. - If readiness or smoke checks fail and the old image is compatible with the unchanged database/config/data, atomically restore the preceding
image.envand restart. Check readiness again. Do not delete the failed artifact or its receipt; retain it for diagnosis. - Release the lock, run/confirm another backup, and update the deployment record. Retain old lazy-loaded web assets across a future deployment strategy or tell editing users to refresh when ready; do not force a reload that discards their work.
Expected brief downtime comes from stopping/restarting the single API container. A later blue/green approach needs old asset retention, database compatibility and capacity beyond this small host.
Schema or configuration releases#
Current PostgreSQL startup requires an exact migration ledger and hashes. Applying a new migration makes an old image's startup verification fail, even when SQL is additive. An older image continuing to run is not proof that it can restart. Never promise automatic image-only rollback after a migration. See database delivery.
- Review the forward migration, runtime table privileges, import/cutover impact and old/new data behavior. Do not edit applied migration files. Rehearse the candidate image with a restored, populated database and private media/native key.
- Choose and test recovery explicitly: a rollback image that includes the new ledger but compatible old application behavior; fix forward; or restoring the pre-migration database under maintenance. A database restore can discard subsequent writes, so stop writes and define the recovery point first. A production restore is not a routine image flip.
- Capture a fresh confirmed backup including roles, configuration/native key, TLS, exact image and S3 byte inventory. Hold the deployment lock through migration and promotion, using the separate protected migration environment/role. The application role must not migrate or gain owner privileges.
- Run the candidate's explicit migration/bootstrap flow from database delivery, reconcile reviewed table grants, start the candidate and verify existing accounts, releases, library/Player grants and new schema paths. A missing or altered ledger must fail before serving requests.
- Perform the agreed recovery if qualification fails. Otherwise release maintenance/lock, verify and create a fresh post-release backup. Record both migration history and running image.
The billing migrations are in source only until this process is performed. Do not deploy a newer billing image against the old pilot ledger, or run a migration as an incidental startup side effect. A future compatible-ledger policy would need version bounds and backward/forward tests; it is not current support.
For configuration changes, retain the prior protected values and stable native authorization key. Validate optional provider configuration before restart. Never copy the developer .env wholesale. Stripe connection/live activation is its own reviewed rollout, not a side effect of deploying the code; see Stripe setup.
Acceptance and monitoring#
Minimum checks: app HTML/assets, sign-in/session continuity, an existing owned draft, public playback, private cross-account denial, invited-upload policy, exact engine/media bytes, library sync and the changed feature. Use bounded original audio; do not require user devices or live purchases to pass automated fixtures. Native changes also require Player/plugin build and declared DAW qualification; web-only changes do not claim that qualification.
Record the app/source/image/engine identities, operator/time, changes, migration/configuration impact, checks, backup version, rollback choice and known limitations. Monitor health, memory, error rate, missing-backup alarms, email and relevant business-event failures after rollout. Budget alerts notify and do not stop resources.
A daily snapshot or uploaded archive is not a proved replacement-host restore. Keep the remaining hosted/browser/DAW recovery gates visible in the deployment record. Restore rehearsal and periodic dependency/OS updates are operational work separate from ordinary feature releases.
Later automation#
Automate this same sequence after manual releases are repeatable: CI checks, immutable reviewed artifacts/receipts, retained versioned S3 archives, target verification, fresh-backup proof, protected promotion credentials, lock/CAS, schema compatibility gates, public smoke checks and explicit recovery. Trigger production promotion deliberately; do not turn every push into a live deployment. Signing credentials and Stripe live keys never enter a browser build or untrusted pull-request runner.
The v0.0.3 continuous delivery plan specifies this automation, and its files have landed disabled: release workflows under .github/workflows/, infra/lightsail/promote.sh, infra/aws/delivery.json and the gate, smoke and publish tools. Nothing runs on push until the repository variable TUNETOYS_RELEASE_PIPELINE is enabled and the enablement checklist is complete. As it stands, an enabled push would check, build and qualify the image on CI and then stop: without the tunetoys-ci role it publishes nothing and reports "gate skipped: no AWS identity". The plan: the same sequence driven from a push to main, with image-only releases promoted automatically once supervised releases pass and schema releases always held for approval.
Related#
- Hosting runbook
- AWS pilot record
- Database runbook
- Release, state and compatibility contract for creator Toy releases