Tag: Release Process

  • Change management for Salesforce releases: why “it works in sandbox” isn’t enough

    I’ve watched a deployment pass every test in sandbox, get signed off by UAT, go to production on a Friday afternoon, and break order processing for six hours. Nobody did anything wrong in the testing phase. The sandbox just wasn’t production, and the gap between the two is where most Salesforce release failures actually live.

    This isn’t an argument against testing in sandbox. It’s an argument for treating “it passed in sandbox” as the start of your confidence, not the end of it.

    What sandbox actually proves

    A sandbox proves your code compiles, your Apex tests pass, your Flow doesn’t throw an error on the records you fed it, and the three people who clicked through UAT didn’t find anything obviously wrong. That’s real value. It’s also a narrow slice of what production will throw at your release.

    Data shape and volume

    Most sandboxes are partial copies, refreshed months ago, seeded with a fraction of production’s record count. A trigger that handles 200 test records cleanly can hit governor limits against 50,000 real ones. A report that renders instantly against a thin dataset can time out against three years of case history. I’ve seen validation rules pass clean in sandbox because the sandbox simply didn’t have the edge-case data combinations that exist in a mature production org, duplicate record types from an old migration, fields left blank by an integration that used to populate them, status values nobody uses anymore but that are still sitting on live records.

    Integration behaviour

    Sandbox integrations are usually pointed at test endpoints, mocked responses, or nothing at all. Your middleware might behave completely differently when it’s talking to the real payment gateway, the real marketing platform, or the real legacy system on the other end. Callout timeouts, rate limits, and authentication token refreshes are exactly the kind of thing that only shows up under real traffic patterns, not a UAT script run by three people over two days.

    User behaviour under real load

    UAT testers follow the happy path you gave them. Real users don’t. They open fifteen tabs, paste data from Excel with hidden characters, hit buttons twice because the page felt slow, and use the org in ways the requirements document never anticipated. None of that shows up until go-live, and by then it’s not a bug report, it’s a support queue.

    Build a release process, not a hope

    The fix isn’t more sandbox testing. It’s treating release management as its own discipline, with its own checks, separate from “did the feature work.”

    Get your environment strategy right first

    If you’re still deploying from a single developer sandbox straight to production, that’s the first thing to fix. A reasonable baseline for an enterprise org looks like: source-tracked developer sandboxes or scratch orgs for build, an integration sandbox for combined testing across teams, a full or partial copy sandbox refreshed regularly for UAT and performance testing, and production. Each stage exists to catch a different class of problem. Skipping a stage doesn’t save time, it just moves the discovery of the problem later and more expensive.

    Automate what should never depend on memory

    Manual change sets, clicked together by whoever’s free that week, are where deployments silently drop a permission set or forget a custom label. A CI/CD pipeline, even a modest one built on Salesforce DX and a free GitHub Actions runner, forces every deployment through the same validated steps: run all tests, check code coverage, validate against the target org before deploying, and keep a record of exactly what went out and when. The point isn’t tooling for its own sake. It’s removing the chance that Friday’s deployment depends on someone remembering a manual step from three releases ago.

    The checks that catch what sandbox misses

    Test against real data volume

    Before a release that touches triggers, flows, or batch logic, run it against a full or recent partial copy sandbox, not a seeded one. If that’s not available, at minimum export enough representative production data to simulate real volume and genuinely messy field values. A load test that takes an afternoon is cheaper than an incident that takes a night.

    Check permission and profile parity

    A feature can work perfectly for the system administrator profile used in sandbox testing and fail completely for the actual end-user profile in production, because a field-level security setting, a permission set assignment, or a sharing rule differs between the two environments. Test with a cloned user matching the real target profile, not your own admin login.

    Write the rollback plan before you deploy, not after it breaks

    Know in advance exactly how you’d undo this release: which metadata to retract, whether any data migration needs reversing, who has the authority to pull the trigger on a rollback at 2pm on a Tuesday. If you can’t answer that in one sentence before deployment, you’re not ready to deploy, regardless of how clean sandbox looked.

    Watch the first hour in production like it’s still testing

    The deployment finishing isn’t the finish line. Monitor error logs, check that scheduled jobs and integrations fired correctly, and have someone specifically watching for the first hour rather than assuming silence means success. Most production incidents I’ve seen were visible in the debug logs within minutes. Nobody was looking.

    What this looks like in practice

    On a typical enterprise Salesforce org I’ve worked with, the releases that went smoothly weren’t the ones with the most UAT sign-offs. They were the ones where someone had deliberately asked “what does production have that sandbox doesn’t,” and then tested against that gap specifically: real data volume, real integration endpoints, real user profiles, and a rollback plan written down before the deployment window opened.

    “Works in sandbox” is a necessary condition for a safe release. It was never a sufficient one. Treat the gap between the two environments as a known risk to manage, not a surprise to react to, and most of your worst release days stop happening.

  • A security review checklist before any Salesforce release

    Every Salesforce team I’ve worked with has a release checklist. Most of them cover deployment steps, test coverage, and rollback plans. Security review is often the item that gets a tick box with nobody actually looking. That’s how a permission set built for a pilot ends up granting “Modify All” to a profile with 200 users, three releases after anyone remembers why it was created.

    I’ve sat in enough post-incident reviews to know the pattern. Nobody set out to expose sensitive fields to the wrong profile. It happened because a change that looked small in isolation, a new field, a cloned permission set, a flow running in system context, never got checked against what it actually opened up once it hit production.

    Why security review keeps getting skipped

    There are a few reasons, and they compound. It takes real judgment, not a script you run and read a green tick from. It doesn’t map neatly to a single owner either: the admin who built the permission set isn’t necessarily the person who understands the object’s data sensitivity, and the developer writing the Apex trigger isn’t necessarily thinking about who can invoke it. And it’s the easiest checklist item to defer when a release date is fixed and something else broke in UAT.

    None of that makes it optional. It just means it needs a checklist specific enough to run in twenty minutes, not a vague instruction to “review security.”

    What to actually check before you ship

    Field-level security and object permissions

    Any new or changed field needs an explicit answer to who can see it and who can edit it, not an assumption that the default profile settings are fine. Check every profile and permission set that touches the object, not just the ones you edited. It’s common for a field to get correctly locked down on the profile you were focused on and left wide open on three others nobody thought to check.

    Sharing model changes

    Organisation-wide defaults, sharing rules, and role hierarchy changes have a habit of solving one visibility problem while quietly creating another. If you’re moving an object from Public Read/Write to Private, walk through every automation, report, and integration that assumes broader visibility. I’ve seen a tightened sharing rule silently break an integration user’s access to records it needed to update, and the failure didn’t surface until a batch job started throwing errors two days later.

    Apex running without sharing

    Classes declared without sharing bypass the sharing model entirely, and that’s sometimes the right call. The problem is when it’s the default because nobody thought about it, not a decision. Same with SOQL and DML: check whether CRUD and field-level security are actually enforced in the code path, especially in anything invoked from a Lightning component or an exposed Apex REST endpoint. Security enforcement should show up somewhere in any query that touches user-facing data, not just the queries someone remembered to protect.

    Integration and API surface

    Every new connected app, named credential, and integration user is a new door into the org. Check the OAuth scopes granted are the minimum the integration actually needs, not whatever was easiest to get working in a sandbox. Check the integration user’s profile and permission sets against what the integration touches today, because it’s common for an integration user to accumulate access over eighteen months that has nothing to do with its current job.

    Guest user access

    If the release touches an Experience Cloud site, the guest user profile deserves its own pass, separate from everything else. Guest users are the one profile in most orgs where broad object access is a genuine security incident waiting to be reported, not a hypothetical. Salesforce has tightened defaults here over the last few releases, but a cloned profile or an older site can still be sitting on access nobody would approve if they saw it written down.

    Flows and automation running in system context

    A flow set to run in system context without sharing ignores both field-level security and the running user’s record access. That’s frequently the right choice for something like a data cleanup job. It’s the wrong choice, more often than teams expect, for a flow triggered from a screen a regular user interacts with, because it means the automation can read and write things that user couldn’t touch directly.

    Where this actually goes wrong in practice

    The releases that cause problems are rarely the big ones. Big releases get scrutiny by default: more eyes, more testing, a slower pace that leaves room to notice things. It’s the small release that skips review, the one field addition or the permission set clone done at 4pm on a Thursday to unblock a stakeholder, because it feels too minor to need it. Those are exactly the changes worth running through a fixed checklist, since a fixed checklist doesn’t care how small the change felt at the time.

    Making this part of the release process, not an extra step

    The checklist above works because it’s specific enough to run against a change in minutes, and specific enough that two different reviewers reach the same conclusion. Put it in the pull request template or the release notes, right next to test coverage. Assign it to whoever owns the release, not “the security team” as an abstract entity, because unowned checklist items are the ones that get skipped first when time runs short.

    None of this needs new tooling. It’s a short, fixed set of questions applied consistently, every release, on every object and permission set the release touches. The org that does this quietly avoids the incident the org that doesn’t ends up explaining after the fact.