# Cicd For Experience Cloud

> CI/CD pipeline patterns for Experience Cloud sites — the layer between sfdx-project metadata deploys and a working production site. Covers GitHub Actions / Jenkins / GitLab pipeline shapes for ExperienceBundle / DigitalExperienceBundle deploys, BrandingSet + ExperiencePropertyTypeBundle promotion, guest-user permission-set automation, custom-domain / CDN binding scripts, and the ordering rules that make the difference between a clean deploy and a half-applied site. NOT for the metadata-shape details (use devops/experience-cloud-deployment-dev), NOT for generic SFDX CI/CD (use architect/ci-cd-pipeline-architecture).

- Skill: `pranavnagrecha/cicd-for-experience-cloud` (Agent Skill, multi-file: 7 files)
- Install (CLI): `npx skillmds add pranavnagrecha/cicd-for-experience-cloud`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pranavnagrecha/cicd-for-experience-cloud/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: PranavNagrecha (https://skillmd.com/u/pranavnagrecha)
- Updated: 2026-09-08
- Page: https://skillmd.com/skills/pranavnagrecha/cicd-for-experience-cloud

---


# CI/CD for Experience Cloud

The pipeline layer that turns a versioned ExperienceBundle into a working
production site. Generic SFDX CI/CD gets you 80 % of the way there; the
last 20 % is Experience-Cloud-specific — metadata ordering rules, theme
promotion, guest-user permission-set automation, and the custom-domain
binding that lives outside Metadata API entirely.

What this skill is NOT. The shape of `ExperienceBundle` /
`DigitalExperienceBundle` metadata, the CMS Managed Content exclusion,
and the per-bundle deploy mechanics live in
`devops/experience-cloud-deployment-dev`. Generic pipeline design
(stage sequence, promotion strategy, quality gates) lives in
`architect/ci-cd-pipeline-architecture`. This skill is the Experience-Cloud-specific
*pipeline-tooling* layer between them.

---

## Before Starting

- **Confirm the metadata API version.** `ExperienceBundle` is the older
  shape; `DigitalExperienceBundle` is the v58+ shape for LWR sites.
  Older sites may still ship as `ExperienceBundle`. The pipeline must
  match the source-org's bundle type.
- **Decide who owns CMS content promotion.** Salesforce CMS Managed
  Content is *not* fully covered by the SFDX CLI / Metadata API.
  Pipelines that assume "deploy bundles, content goes with them" will
  ship broken sites. Either treat CMS content as runtime-config
  (admins promote separately) or build a custom CMS-content-export
  step.
- **Identify every dependency the bundle deploy needs in place first.**
  Custom objects, fields, profiles, permission sets, the Network
  metadata that defines the site itself — all must exist in the
  target before the bundle deploys. Order matters.
- **Decide the guest-user model.** Permission-set assignment to the
  guest user is a one-time-per-org operation that bundle deploys do
  NOT redo. If the pipeline doesn't include explicit guest-user
  reconciliation, the first guest after a fresh deploy will see "no
  access" errors that nobody knows how to debug.

---

## Core Concepts

### The four metadata-ordering rules

Experience Cloud deploys are non-trivially ordered. The pipeline must
sequence:

1. **Foundation metadata first.** Custom objects, custom fields,
   permission sets, profiles, profile field-level security. The
   ExperienceBundle references these; if missing, the deploy fails.
2. **`Network` metadata.** Defines the Site / Community itself
   (URL prefix, login behavior, accessibility). Required before any
   ExperienceBundle that targets it.
3. **`ExperienceBundle` / `DigitalExperienceBundle`.** The bundle
   itself — pages, components, properties, navigation menus.
4. **`BrandingSet` and `ExperiencePropertyTypeBundle`.** Theme
   tokens (colors, fonts, logos), component property types. Often
   shared across multiple sites — promote *after* the bundles that
   reference them, but the BrandingSet metadata itself can deploy in
   the same package.

CMS Managed Content (articles, images, video assets) is **outside this
ordering** — handled separately because it doesn't ship via SFDX.

### Guest-user permission-set automation

Every Experience Cloud site has a guest-user profile auto-provisioned
when the site is created. Permissions on the guest user control what
anonymous visitors can read. The pipeline must:

- Assign required permission sets to the guest user (most common:
  read-access to lookup objects the site queries).
- Re-apply permission sets after any production refresh / sandbox
  copy that resets the guest user.
- Validate guest-user permissions match a known baseline before
  promoting through environments.

Salesforce CLI does not have a single "set up the guest user" command.
The automation pattern is a small sf-cli wrapper script (or Apex
anonymous block) that assigns the relevant permission sets to the
site's guest user, idempotent on re-run.

### Custom-domain / CDN binding

Production Experience Cloud sites usually live on a custom domain
(`portal.example.com`, not `example.my.site.com`). Two pieces:

1. **Salesforce-side:** Setup → Domains → register the custom domain
   → bind to the site. Metadata-deployable via `CustomDomain` /
   `CustomSite` types but with caveats — the domain registration
   step requires DNS verification that's not pipeline-automatable on
   the Salesforce side.
2. **DNS-side:** CNAME the custom domain at the Salesforce-supplied
   target (or a CDN that fronts it). Owned by the DNS team, not
   Salesforce.

The pipeline coordinates these: deploy the Salesforce-side metadata,
emit the DNS targets to a known location, the DNS team applies them.
Many pipelines stop at "metadata deployed" and miss that the site
isn't reachable until DNS propagates — explicit DNS-confirmation gate
prevents that.

### Theme reuse via BrandingSet

`BrandingSet` defines tokens (primary color, font family, logo
references) that themes consume. A single BrandingSet referenced from
multiple sites is the right pattern for multi-site brand consistency.
Pipeline implication: BrandingSet promotion is a separate concern from
bundle promotion — same metadata package can carry both, but the
theme team's promotion cadence often differs from the
site-feature-team's cadence.

---

## Common Patterns

### Pattern A — GitHub Actions pipeline for a single LWR site

```yaml
# .github/workflows/experience-cloud-deploy.yml
name: deploy-experience-cloud-site

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: install sf cli
        run: npm install -g @salesforce/cli

      - name: authenticate
        env:
          SFDX_AUTH_URL: ${{ secrets.SFDX_AUTH_URL }}
        run: |
          echo "$SFDX_AUTH_URL" > ./auth.txt
          sf org login sfdx-url --sfdx-url-file ./auth.txt --alias prod
          rm ./auth.txt

      # 1. Foundation: objects/fields/permission-sets the bundle depends on.
      - name: deploy foundation metadata
        run: sf project deploy start
              --source-dir force-app/main/default/objects
              --source-dir force-app/main/default/permissionsets
              --target-org prod

      # 2. Network metadata (defines the site itself).
      - name: deploy network metadata
        run: sf project deploy start --source-dir force-app/main/default/networks --target-org prod

      # 3. The ExperienceBundle / DigitalExperienceBundle itself.
      - name: deploy experience bundle
        run: sf project deploy start --source-dir force-app/main/default/experiences --target-org prod

      # 4. BrandingSets last — they reference bundle artifacts.
      - name: deploy branding sets
        run: sf project deploy start --source-dir force-app/main/default/brandingSets --target-org prod

      # 5. Guest-user permission reconciliation.
      - name: assign guest-user permsets
        run: bash scripts/assign-guest-permsets.sh prod

      # 6. Smoke test — confirm site loads, anonymous read works.
      - name: smoke test
        run: bash scripts/smoke-test-site.sh prod

      # 7. Emit DNS targets for the DNS team (if domain not yet bound).
      - name: emit DNS targets
        run: bash scripts/emit-dns-targets.sh prod > dns-targets.txt
      - uses: actions/upload-artifact@v4
        with:
          name: dns-targets
          path: dns-targets.txt
```

The seven-step shape: foundation → Network → bundle → BrandingSet →
guest-user → smoke → DNS-emit. Drop or reorder a step and the deploy
either fails noisily (good) or appears to succeed but leaves the site
broken (bad — see Gotchas).

### Pattern B — Multi-site theme promotion

**When to use.** A BrandingSet shared by 4+ sites (corporate brand),
promoted on its own cadence (theme team owns it) separate from the
per-site feature releases.

**Approach.** Two pipelines, one repository:

- `branding-pipeline.yml` — deploys `force-app/main/default/brandingSets/`
  alone. Triggers on changes to that directory. Runs against all
  consuming orgs.
- `site-pipelines/<site>.yml` — deploys per-site bundles. Trigger on
  per-site directory changes.

The pipelines are independent; site bundles reference the BrandingSet
by API name. Coordination happens via the API name being stable —
renaming a BrandingSet is a coordinated migration across every site
that consumes it.

### Pattern C — Guest-user permission-set baseline validator

**When to use.** Production refreshes / sandbox copies that reset
guest-user permissions. Without a validator, you discover the
regression when a customer reports "site says no access".

```bash
#!/bin/bash
# scripts/validate-guest-permsets.sh
set -eu
TARGET=$1
EXPECTED_PERMSETS="Site_Public_Read,Site_Lookup_Access"

# Find the site's guest user.
GUEST_USER=$(sf data query --query \
    "SELECT Username FROM User WHERE Profile.Name LIKE '%Site Profile%' AND IsActive = TRUE LIMIT 1" \
    --target-org "$TARGET" --json | jq -r '.result.records[0].Username')

# Check assigned permission sets.
ASSIGNED=$(sf data query --query \
    "SELECT PermissionSet.Name FROM PermissionSetAssignment WHERE Assignee.Username = '$GUEST_USER'" \
    --target-org "$TARGET" --json | jq -r '.result.records[].PermissionSet.Name' | sort | tr '\n' ',')

# Compare against baseline.
for ps in ${EXPECTED_PERMSETS//,/ }; do
    if ! echo "$ASSIGNED" | grep -q "$ps"; then
        echo "MISSING guest permset: $ps"
        exit 1
    fi
done
echo "OK: guest user has all expected permsets"
```

Run as a CI gate on every promotion. Cheap; catches the regression
before customers do.

---

## Decision Guidance

| Situation | Approach | Reason |
|---|---|---|
| First Experience Cloud pipeline for a new site | **Pattern A** (single-site 7-step pipeline) | Standard shape; covers ordering, guest user, DNS coordination |
| Multi-site brand consistency | **Pattern B** (theme pipeline + per-site pipelines) | BrandingSet promotion has a different cadence than site features |
| Refreshed sandbox loses guest permissions | **Pattern C validator** as CI gate | Catches the regression automatically |
| LWR site (modern, post-API v58) | `DigitalExperienceBundle` deploys | LWR-specific bundle type |
| Aura-based site | `ExperienceBundle` deploys | Older bundle type |
| Hybrid Aura + LWR site (uncommon) | Both bundle types in the package | Each component lives in its own bundle |
| Custom domain not yet DNS-bound | Pipeline emits DNS targets; gate on confirmation | Don't promote past "metadata deployed" until the site is reachable |
| CMS Managed Content needed in target | Separate CMS-content-export step (NOT SFDX) | CMS isn't covered by Metadata API |
| Generic CI/CD pipeline design without Experience-Cloud specifics | **Use `architect/ci-cd-pipeline-architecture`** | Don't reinvent the pipeline layer |
| Bundle metadata details / shape | **Use `devops/experience-cloud-deployment-dev`** | This skill is pipelines, not bundle internals |

---

## Recommended Workflow

1. **Identify bundle type** (Aura `ExperienceBundle` vs LWR `DigitalExperienceBundle`). Pipeline source paths and deploy commands differ.
2. **Inventory dependencies** (objects, fields, profiles, permission sets, Network metadata) and order them as foundation → network → bundle → branding.
3. **Build the pipeline** following Pattern A's seven-step shape.
4. **Add guest-user reconciliation** as an explicit step. Don't assume it's automatic.
5. **Add the guest-user permset validator** (Pattern C) as a CI gate.
6. **Coordinate DNS** with the DNS team — pipeline emits targets, DNS team applies, gate the next promotion on DNS-confirmation.
7. **Plan CMS-content promotion separately** if the site uses CMS Managed Content.

---

## Review Checklist

- [ ] Bundle type matches the source-org's site (ExperienceBundle vs DigitalExperienceBundle).
- [ ] Foundation metadata (objects, fields, permission sets) deploys *before* the bundle.
- [ ] Network metadata deploys before the ExperienceBundle that targets it.
- [ ] BrandingSet promotion is sequenced with the bundles that reference it.
- [ ] Guest-user permission-set assignment is an explicit pipeline step.
- [ ] Guest-user permset validator runs as a CI gate post-deploy.
- [ ] CMS Managed Content promotion is handled (or explicitly excluded with rationale).
- [ ] Custom-domain DNS targets are emitted and the DNS team has a known follow-up.
- [ ] Smoke test validates the site loads and anonymous read works.

---

## Salesforce-Specific Gotchas

1. **CMS Managed Content is not covered by SFDX.** Pipelines that assume "deploy and the content comes too" ship broken sites. (See `references/gotchas.md` § 1.)
2. **Guest-user permission sets reset on sandbox refresh.** Without a CI step that re-applies them, the first guest after refresh sees access errors. (See `references/gotchas.md` § 2.)
3. **Network metadata must deploy before the ExperienceBundle** that targets it. Reverse order produces a deploy that succeeds-but-doesn't-work. (See `references/gotchas.md` § 3.)
4. **`ExperienceBundle` (Aura) and `DigitalExperienceBundle` (LWR) are different metadata types.** Pipeline source paths differ; mixing them in a single deploy is fine but the paths and target sites must match. (See `references/gotchas.md` § 4.)
5. **Custom-domain registration requires DNS verification** that's not pipeline-automatable on the Salesforce side. The pipeline must coordinate with the DNS team. (See `references/gotchas.md` § 5.)
6. **`ExperienceBundleSettings` must be enabled in the target org** before any ExperienceBundle deploys. First-time setup catches this once. (See `references/gotchas.md` § 6.)
7. **Renaming a BrandingSet API name breaks every site that references it.** Treat as a coordinated migration. (See `references/gotchas.md` § 7.)

---

## Output Artifacts

| Artifact | Description |
|---|---|
| Pipeline definition | `.github/workflows/*.yml`, Jenkinsfile, GitLab CI YAML, etc. — the seven-step shape |
| Guest-user assignment script | sf-cli wrapper that assigns permission sets to the site guest user |
| Guest-user validator | CI gate that confirms the guest user has the expected permission sets |
| DNS-target emitter | Post-deploy script that surfaces the Salesforce-supplied CNAME target for the DNS team |
| Smoke-test script | Confirms the site URL responds 200 and the homepage renders for an anonymous visitor |

---

## Related Skills

- `devops/experience-cloud-deployment-dev` — bundle metadata internals (this skill ends where that one starts).
- `architect/ci-cd-pipeline-architecture` — generic CI/CD stage sequence and quality gates; this skill assumes those primitives are in place.
- `admin/experience-cloud-site-setup` — site design / configuration; this skill is the deploy half.
- `security/guest-user-security` — what permissions the guest user *should* have (vs this skill which is about how the pipeline applies them).
- `architect/experience-cloud-licensing-model` — license-model decisions upstream of the pipeline.

