Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SF Org-to-Org Metadata Sync — Production Setup

Folder structure

sf-org-sync/
├── .github/
│   ├── CODEOWNERS                   # requires review on config/manifest/workflow changes
│   └── workflows/
│       ├── sf-org-sync.yml          # the pipeline
│       └── validate-sync-config.yml # validates config/sync-config.yml on every PR
├── config/
│   └── sync-config.yml              # the full/partial flag — changed via PR, read by the pipeline
├── force-app/
│   └── main/default/                # retrieved metadata lands here (source format)
├── manifest/
│   ├── package.xml                  # scope for "partial" syncs
│   ├── package-full.xml             # auto-generated by "full" runs
│   └── excluded-metadata.txt        # metadata types NEVER touched, even in full mode
├── scripts/
│   └── filter_manifest.py           # strips excluded types out of an auto-generated full manifest
├── certs/                           # LOCAL ONLY — JWT keys, never committed (see .gitignore)
├── logs/                            # deploy result JSON, one per run, committed for audit trail
├── sfdx-project.json
├── .forceignore
├── .gitignore
└── README.md                        # this file

1. One-time setup per org (source and target)

  1. Create a self-signed certificate locally (keep the .key off any shared drive):
    openssl req -x509 -newkey rsa:2048 -keyout certs/server.key -out certs/server.crt -days 365 -nodes
    
  2. Setup → App Manager → New Connected App → enable OAuth → check "Use digital signatures" → upload server.crt.
  3. Note the Consumer Key.
  4. Create a dedicated integration user (not your personal login) with only the permissions retrieve/deploy needs. Assign it the Connected App's permitted profile/permission set.
  5. Repeat for the other org. You'll end up with two certs, two Connected Apps, two integration users.

2. GitHub repo secrets

Settings → Secrets and variables → Actions → New repository secret:

Secret Value
SOURCE_SF_JWT_KEY contents of source server.key (full PEM block)
SOURCE_SF_CONSUMER_KEY source Connected App consumer key
SOURCE_SF_USERNAME source integration user username
SOURCE_SF_INSTANCE_URL e.g. https://yourdomain.my.salesforce.com
TARGET_SF_JWT_KEY contents of target server.key
TARGET_SF_CONSUMER_KEY target Connected App consumer key
TARGET_SF_USERNAME target integration user username
TARGET_SF_INSTANCE_URL target org instance URL
SLACK_WEBHOOK_URL (optional) incoming webhook for failure alerts

3. How the pipeline actually works now (PR-reviewed deploys)

This is split into two independent workflows so a human always reviews what's about to be deployed:

sf-pull-and-open-pr.yml (runs on schedule, 10 AM IST daily)

  1. Authenticates to the source org.
  2. Retrieves metadata per config/sync-config.yml (full or partial).
  3. If anything changed, commits it to a new pull/<mode>-<timestamp> branch and opens a Pull Request against main — it does NOT merge anything itself.
  4. If nothing changed, it just logs that and stops. No PR, no noise.

Your team reviews the PR like any other PR — reads the metadata diff, decides what's safe to ship, and either:

  • Merges it as-is,
  • Requests changes / manually edits the branch to strip out anything risky, then merges,
  • Or closes it without merging if none of it should go out.

sf-deploy-on-merge.yml (fires automatically the moment a PR is merged into main)

  1. Triggers on push to main — but only when files under force-app/ or manifest/ actually changed (a merge that only touches, say, README.md won't trigger a deploy).
  2. Waits for approval on the production GitHub Environment (same reviewer gate as before).
  3. Runs sf project deploy validate as a dry run.
  4. Deploys for real, logs the result into logs/, and pushes that log back to main.
  5. Posts to Slack on failure.

Net effect: nothing ever reaches the target org without a human having merged a PR that a human could read first. The schedule only controls how often a proposal shows up — not when anything actually deploys.

4. Changing full vs. partial, and which manifest, via pull request

Two independent flags live in config/sync-config.yml:

retrieve_mode: partial      # or: full
manifest_flavor: package    # or: custom (only used when retrieve_mode is partial)
  • retrieve_mode: full ignores manifest_flavor entirely and auto-generates a manifest from everything in the source org (filtered by manifest/excluded-metadata.txt).
  • retrieve_mode: partial uses one of two hand-maintained manifests, chosen by manifest_flavor:
    • package -> manifest/package.xml (currently: Payment__c and its fields only)
    • custom -> manifest/package-custom.xml (currently: Payment__c AND Account, plus their fields)

To change either flag, or to change what's actually inside package.xml / package-custom.xml (e.g. adding more objects to the custom manifest):

  1. Open a PR editing config/sync-config.yml and/or the manifest file(s) directly.
  2. validate-sync-config.yml checks both flag values are valid and comments on the PR with what will result.
  3. CODEOWNERS requires review before merge.
  4. Once merged, the next scheduled pull (or a manual run left on use-config) picks up whatever is now on main — both which manifest is used AND what's inside it.

A manual workflow_dispatch run can still override retrieve_mode for a one-off test without touching the config file, but manifest_flavor is always read from the config file directly (there's no dropdown override for it, since it's expected to change less often than a one-off test run would need).

5. Production safeguards (already wired into the workflow)

  • GitHub Environment gate: the job specifies environment: production. Go to Settings → Environments → New environment → name it production → add required reviewers. This means every run (including the scheduled one) pauses and waits for a human to click "Approve" before it touches your target org. Remove this if you want it fully unattended — but for a production target, keep it.
  • Validate before deploy: sf project deploy validate runs as a dry-run before the real deploy, so a bad deployment is caught without touching the org.
  • Full-mode filtering: scripts/filter_manifest.py strips anything listed in manifest/excluded-metadata.txt (Profiles, PermissionSets, sharing rules, security settings, etc.) out of an auto-generated full manifest before it's ever retrieved or deployed. Edit that file to tune what's excluded.
  • Concurrency lock: only one sync can run at a time, so a manual trigger can't collide with the scheduled run.
  • Branch-per-run audit trail: every pull and every deploy gets its own branch (pull/partial-20260709-1030, deploy/full-20260709-1030) before merging into main, so you can always trace exactly what changed and when.
  • Deploy logs committed: each deploy's JSON result lands in logs/, committed to main, so deploy IDs and test results are searchable in git history.
  • Slack alert on failure: any step failing posts a link to the failed run.

6. Branch protection (set this up manually)

Settings → Branches → Add rule for main:

  • Require status checks to pass (once you add a lint/validate-only job for PRs).
  • Restrict who can push directly — the bot pushes via contents: write permission on the GITHUB_TOKEN, which is fine; just make sure no human pushes untested changes straight to main.

7. Testing before trusting the cron schedule

  1. Run the workflow manually (workflow_dispatch) with retrieve_mode: partial against a sandbox target first, not production.
  2. Approve the environment gate when prompted.
  3. Confirm the pull/ and deploy/ branches appear, and that logs/ has a deploy result committed.
  4. Only then point TARGET_SF_INSTANCE_URL at production and let the cron schedule take over.

8. Ongoing maintenance

  • Add new metadata types to manifest/package.xml as your org grows — this is the file that governs the day-to-day scheduled sync.
  • Review manifest/excluded-metadata.txt whenever you run a full sync for the first time in a new org, since orgs vary in what metadata types are risky to move unattended.
  • Rotate the JWT certificates annually (or per your org's security policy) and update the GitHub secrets accordingly.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages