What a passing test proves in dev, QA and staging
Green runs in QA or staging predict production as far as the differences between the environments allow. Check these first: the SQL Server edition and collation, MySQL's sql_mode, the server clock, and whether the build that shipped is the build that passed.
Five environments and the question each one answers
A pass in any environment answers the one question that environment exists to ask. In QA, a green regression run says the build met its acceptance criteria against QA's database and dataset. Whether the deploy script works with production's service accounts is a separate question, and staging is where it gets asked.
QA turns over faster than dev. A validation or regression run needs a fresh copy with a specific dataset, while a developer's environment lasts as long as the branch and exists to keep one person's code away from a colleague's until merge. Petrichor engineers configure dev, UAT, QA and QC, and production release as separate environment types, because each one runs differently and every team runs them its own way.
| Environment | Question a pass answers | Used by | Lifetime | Has to match production |
|---|---|---|---|---|
| Dev | Does this change work well enough to open a pull request? | One developer | As long as the branch | Database engine and major version |
| QA and QC | Does this build meet its acceptance criteria and pass regression? | The QA team | One test cycle, rebuilt with a named dataset | Engine, edition, collation, operating system and configuration keys |
| UAT | Will the person who asked for the change accept it? | A business owner or the customer | Stable for the length of the review | A build and data the reviewer recognizes |
| Staging | Will this exact artifact deploy and start the way production does? | The release owner | Long-lived, refreshed before each release | Everything except capacity, with production-sized data |
| Production | Can customers do their work on this release? | Customers | Permanent | The reference for every row above |
Match the database edition before the server size
When a build passes QA and fails in production, check the server settings before CPU and memory. On Microsoft SQL Server, start with the edition.
Developer edition, renamed Enterprise Developer in SQL Server 2025, includes every Enterprise feature. A production server licensed for Standard does not, and some of that gap fails loudly. Rebuilding an index WITH (ONLINE = ON) in a migration stops on Standard with error 1712: "Online index operations can only be performed in Enterprise edition of SQL Server."
The rest of the gap changes timing and query plans. On Enterprise, adding a NOT NULL column with a constant default changes only the table's metadata and finishes almost at once, whatever the row count. Microsoft's ALTER TABLE reference ties that shortcut to Enterprise edition, so on Standard the same statement writes the default into every existing row while ALTER TABLE holds its schema modification lock. Enterprise also matches queries to indexed views automatically. Standard uses an indexed view only when the query names it with the NOEXPAND hint, so a report that ran fast in QA reads the base tables in production.
SQL Server 2025 added a free Standard Developer edition, and Microsoft suggests it for certifying a Standard upgrade in a staging environment. On SQL Server 2022 and earlier, catching these differences takes a Standard instance in QA or staging. SQL Server 2025 also caps Standard at the lesser of 4 sockets or 32 cores and at 256 GB of buffer pool, while Developer edition uses whatever the operating system allows. A Developer instance on a QA server with more memory than that can cache a working set that the Standard production server has to read from disk.
To see which engine a test ran against, run SELECT SERVERPROPERTY('EngineEdition') on both servers. It returns 3 for Enterprise, Developer, Enterprise Developer and Evaluation, and 2 for Standard and Standard Developer.
Collation comes next, because it is fixed when the instance is installed. Setup takes the default from the operating system locale, SQL_Latin1_General_CP1_CI_AS on a US English server. A restored database keeps its own collation, and temporary tables live in tempdb under the instance collation. Wherever the instance and database collations differ, a procedure that joins a temp table to a user table on a character column fails with error 468, "Cannot resolve the collation conflict". Changing an instance collation later means exporting the data, dropping the user databases and rebuilding master with setup /ACTION=REBUILDDATABASE and a new /SQLCOLLATION setting. Petrichor's provisioning sequence installs the database software before the application and the dataset, so the collation is fixed at install. Put production's collation in the configuration.
MySQL's sql_mode and the server clock can also pass a test in QA and fail it in production. For each row below, run the query in the last column on both servers and compare the output.
| Difference | What QA shows | What production does | Run on both |
|---|---|---|---|
| SQL Server edition | Developer edition runs the migration and the report | Standard returns error 1712, rewrites rows on ADD COLUMN and skips indexed views without NOEXPAND | SELECT SERVERPROPERTY('EngineEdition') |
| Instance collation | Temp table joins pass because the instance matches the database | The same join returns error 468 because the collations differ | SELECT SERVERPROPERTY('Collation'), DATABASEPROPERTYEX(DB_NAME(), 'Collation') |
MySQL sql_mode | A relaxed sql_mode copied into my.cnf lets a loose GROUP BY return rows | The MySQL 8.4 default includes ONLY_FULL_GROUP_BY and rejects the query | SELECT @@GLOBAL.sql_mode; |
| Server clock | QA's OS time zone is UTC, so GETDATE() returns the UTC date | Production's OS time zone is US Central, so GETDATE() returns a different date for five or six hours of every day | SELECT SYSDATETIMEOFFSET(); |
Staging on production's accounts, secrets and data
A staging pass says this artifact deploys and starts with production's configuration layout, secrets store, service accounts and deploy procedure. That holds only when the configuration matches exactly. Capacity can be smaller, unless staging also carries the load test.
Schema migrations get timed here too. A migration that finishes in a second on a QA dataset of a few thousand rows can hold a schema lock through a full table rewrite at production's row count, and on Standard the NOT NULL column described above is one of those migrations. Load a production-sized copy into staging, masked the way your data owner approved, time the migration, and write the duration into the release note. If it runs past the maintenance window, the release needs a different plan.
If production is on AWS and depends on RDS or Aurora, SQS, IAM roles, or a partner that allowlists production's outbound IP addresses, staging needs those services and that network path, so it stays on AWS. A staging environment anywhere else rehearses a different system.
Dev and QA have fewer ties to production's services. Most of their tests check the application against its own database and a known dataset, and those tests give the same answer wherever the servers sit, provided the edition, collation and version match. QA is usually the first environment Petrichor moves, and a team that does not like the result can pull it back at low risk. When a customer offers production on the first day, our engineers slow that down until they know the team and the application.
Promote the digest QA passed
The build that passed QA has to be the build that ships. A tag moves whenever someone pushes to it, including a version tag pushed twice. The digest names one image and cannot change. Record the digest when the QA run finishes and deploy registry.example.com/app@sha256:<digest> to staging and production.
Software that ships as an installer follows the same rule. Hash the MSI or package when QA signs off, with Get-FileHash -Algorithm SHA256 in PowerShell or sha256sum on Linux, and compare the hash on the staging and production servers before running the installer.
Connection strings, endpoints, feature flags and secrets come from the environment at deploy time and stay out of the artifact. If promoting from QA to staging requires a rebuild, production gets a build QA never tested.
Name who approves the production deploy and what evidence they read. GitHub environments can require up to six users or teams as reviewers, stop people approving their own deploys, add a wait timer of up to 30 days and limit which branches or tags deploy. Which of those you get depends on the plan. On GitHub Free, Pro and Team, required reviewers and wait timers work only in public repositories, so a private repository on Team gets deployment branch rules and has to move to GitHub Enterprise before it can require an approver.
The evidence can be one release note: the digest or hash, the QA run it passed, the dataset version, the migration time from staging, and any difference from production the team accepted. QA teams at one Petrichor customer destroy each test matrix after its run and keep the baseline results as history, so the release note can link the run and its baseline.
When QA can carry the final release check
A separate staging environment earns its cost when the deploy itself carries risk. Teams with one deployable, small additive migrations and a deploy script that runs identically everywhere can fold staging into QA, as long as the last QA pass runs on the release digest with production's configuration layout and database edition. The cost is serial releases: build N+1 waits while build N gets its final check.
Keep staging separate if any of these apply: migrations touch large tables, several services deploy in a fixed order, the installer adds Windows services or registry keys, or QA runs a different SQL Server edition from production. UAT can often share staging as a booked window, with a rule that nobody deploys over the build the reviewers are using.
Book a call with a consultant and bring the QA environment you would move first.