experience-ui-bundle-deploy
MUST activate when the project has a uiBundles/*/src/ directory and the task involves deploying to an org or post-deploy org setup. Deploys a UI bundle app and runs ordered setup: org auth, build, metadata deploy, permission-set and role assignment, Experience Cloud self-registration, social login / SSO / IDP linking (Auth Providers + SAML on a React site), site logout URL, seed-data import, and GraphQL schema fetch + codegen. Trigger signals: *.network-meta.xml, org-setup.config.json (socialLogin/logoutUrl), data-plan.json, sfdx-project.json, or mentions of deploy, org setup, social login/SSO, or logout URL on an Experience site. DO NOT TRIGGER when: creating a new UI bundle project (use experience-ui-bundle-project-generate); styling pages without deploying (use experience-ui-bundle-frontend-generate); adding a feature such as auth, search, or file upload without deploying (use the matching experience-ui-bundle-*-generate skill); configuring MFA permission sets (use experience-ui-bundle-mfa-configure).
- 5,232
- installs
- 0 up / 0 down, score 0.00
- votes
- 1,109
- repo stars
- Updated
- less than a month ago
- In the catalog since
- 1 Oct 2026
- From
- forcedotcom/sf-skills
Skill instructions
Rendered from the published SKILL.md, 332 lines.
Deploying a UI Bundle App
Deploy order is load-bearing: a step's output is the next step's precondition
(deploy before schema fetch; permissions before schema fetch; role/self-reg
before the schema the guest user must see). This is the canonical setup sequence,
ported from the reference org-setup.mjs. The org-setup.mjs line
citations in references/ are port-provenance (why each rule exists) pointing at
that external reference script — not files shipped with this skill — so you don't
need to open them to run the steps.
Run each step in order. Every optional step is presence-driven: if its convention file is absent, no-op cleanly and move on — do not fabricate config. For the two destructive/expensive steps (self-registration, data import), ask the user before running.
Inputs to gather up front
Read these from the project; ask the user only for what's missing:
- Target org — alias/username for
--target-org. Ask if not obvious. - Source root — run
scripts/get-source-root.shto resolve the metadata source dir fromsfdx-project.json(packageDirectories[0].path+/main/default). It exits non-zero if the project file is missing or malformed. Never hardcodeforce-app/main/default. org-setup.config.json(optional) — drives permset assignment, role, self-registration, and social login. Absent keys mean "skip that step". Exception: if the file is missing butpermissionsets/has permsets to assign, don't silently skip — scaffold the config or gather equivalent inputs (see step 4).data-plan.json(optional, in the project'sdata/dir) — presence enables the data step.
Step 1 — Org authentication (always)
Unconditional precondition; cannot be skipped. If the org is already connected
(sf org display --target-org <org> --json succeeds), no-op. Otherwise:
sf org login web --alias <org>
A failed login aborts the whole setup before deploy.
Step 2 — Pre-deploy UI bundle build
Build every UI bundle so dist/ exists before metadata deploy (UI bundle
entities deploy the built output). For each bundle dir under uiBundles/:
npm install
npm run build
Run when deploying UI bundles and dist/ is missing or source changed.
Step 3 — Deploy metadata
If self-registration is configured:
- Deploy license pre-check first (see
references/license-checks.md) — it blocks the deploy with a clear, license-naming message instead of a cryptic failure. - Add the self-reg profile to
networkMemberGroupson the local source — apply Edit A ofassets/network-selfreg-xml-recipe.md. This must happen before this deploy so the profile ships as a recognised site member; do NOT deploy the network file on its own here (this deploy ships it). Best-effort and idempotent — skip if already a member.
Then deploy the whole project (all metadata) by pointing --source-dir at the
resolved source root:
sf project deploy start --source-dir <sourceRoot> --target-org <org>
<sourceRoot> is the value from scripts/get-source-root.sh (e.g.
force-app/main/default). Always pass --source-dir. Do NOT run bare
sf project deploy start with no path: that command relies on source-tracking to
decide what to deploy, and on an org without source-tracking (most non-scratch
orgs) it aborts with "This org does not have source-tracking enabled … specify
the files or a manifest to deploy." Passing --source-dir deploys the same full
set on both source-tracked and non-tracked orgs and never emits that hint. If the
deploy reports conflicts on a source-tracked org, re-run with --ignore-conflicts
— do NOT roll back or reduce the deployed set.
Do NOT hand-build a package.xml, assemble a --metadata-dir mdapi zip, or
otherwise convert to metadata-format — none of that is needed and it is not part
of this flow.
Timeout 180s. Must complete before permission assignment and schema fetch — objects, fields, and permission sets appear in the org only after deploy.
Step 3b — Set the site logout URL (config-gated)
Run only when org-setup.config.json has a top-level logoutUrl. If absent,
no-op cleanly and say so. Non-destructive and idempotent — no ask needed.
Runs here, after the deploy (not folded into it) because the platform rejects
a relative logout URL ("The logout page URL must be an absolute URL."), and a
shipped site-relative path (e.g. /propertyrentalapp/) is resolved to absolute
against the site's Experience Cloud origin — which only exists once the site is
deployed.
Why it matters: a site with no <logoutUrl> sends a logging-out member to the
org default-site login page — in a multi-site org that's a different
community, so Sign Out lands on the wrong site's login page. Setting it steers
logout back to this site (reload as Guest).
Steps:
Read
logoutUrlfrom config; absent → skip.Derive the site —
scripts/derive-site-name.sh(single*.network-meta.xml; skip if zero/ambiguous).Resolve, set, and deploy — one command. Invoke the helper, which resolves the value to an absolute URL (site-relative → matched against the community
siteUrlpath, never guessing), writes<logoutUrl>idempotently in the canonical position, and deploys only that file:node scripts/set-logout-url.mjs \ --logout-url "<logoutUrl from config>" \
Show the whole skill (212 more lines)
--network-file <sourceRoot>/networks/<site>.network-meta.xml \
--target-org <org> --site <site> --deploy
**Best-effort:** the helper exits **0** on success or already-set, and **3** on a
recoverable skip (network file missing, origin unresolvable, XML-special char, or
deploy failure). Treat **exit 3 as a loud skip, not a setup failure** — continue
the rest of setup and tell the user to set the logout URL manually in the site's
Administration settings. Usage, exit-code contract, and port-provenance:
`references/logout-url.md`.
## Step 4 — Assign permission sets
Discover permission sets under `<packageDir>/main/default/permissionsets/`. If
none exist and none were passed explicitly, skip.
**If permsets exist but `org-setup.config.json` is missing, do NOT silently
skip.** A missing config makes every discovered permset resolve to `skip`, so
nothing gets assigned and the later GraphQL schema comes back incomplete (the
caller lacks FLS). Instead, help the user supply the assignments — either scaffold
`org-setup.config.json` from `assets/org-setup.config.template.json` or gather the
per-permset assignee inputs for a one-off run. Full schema + scaffolding flow:
`references/config-scaffold.md`. Confirm intent before writing the file or
assigning — don't fabricate assignees.
Otherwise assign each per its config assignee (`org-setup.config.json` →
`permsetAssignments`), where each assignee is one of `currentUser`, `guestUser`,
or `skip` (default `skip`):
```bash
sf org assign permset --name <permset> --target-org <org> [--on-behalf-of <guestUsername>]
- currentUser — omit
--on-behalf-of. - guestUser — resolve the site's guest username first (see the guest-user
section in
references/self-registration.md). If the site can't be derived or no guest user resolves, skip that permset and record the reason — don't abort the others. - Treat "Duplicate … PermissionSet" and "not found … target org" as skips, not failures.
Required so GraphQL introspection returns the correct schema (the caller needs FLS on custom fields).
Step 5 — Assign role (config-gated)
Run only when org-setup.config.json has role: { assignee: "currentUser", roleName: "<UserRole>" }. Assigning a role to the current user is what lets
Experience Cloud self-registration work. Idempotent — skip if the user already
has a role. Detail + exact queries: references/role-assignment.md.
Step 6 — Enable self-registration (config-gated) — ask first
Run only when org-setup.config.json has
selfRegistration: { selfRegProfile, accountName }. Ask the user before
running. Sequence (full detail in references/self-registration.md):
- License pre-check (soft skip) — if the org lacks a seat on the profile's
license, warn and skip; it is not a failure. See
references/license-checks.md. - Derive the site — run
scripts/derive-site-name.sh; it outputs the site name (the base name of the single*.network-meta.xml) or exits non-zero when zero or more than one exist (ambiguous — stop). - Flip self-reg on + redeploy the network file — apply Edit B of
assets/network-selfreg-xml-recipe.md(setselfRegistration=true, inject<selfRegProfile>), then redeploy only that one file. Idempotent — skip both if already enabled. (Edit A, the member-group add, already happened in step 3.) - Create the Account + NetworkSelfRegistration — apply
assets/network-selfreg.apex(idempotent; both are query-then-create; run 4a and 4b as two separatesf apex runinvocations).
Step 6b — Enable social login (config-gated)
Run only when org-setup.config.json has a socialLogin block. If the block is
absent, loadSocialLoginConfig returns null and the step is hidden — no-op
cleanly and say so. This step is non-destructive and idempotent (it only adds
missing links/members), so — unlike self-registration and data import — it does
not require asking first.
This links the site's configured Auth Providers (OAuth) and SAML SSO
configs to the site so the built-in Social Login component renders their
buttons on the React login page. On React (Site Container) sites the SSO admin UI
is hidden, so the linking is done programmatically via AuthConfig /
AuthConfigProviders records — you cannot do it by clicking through Setup. Full
detail, sub-steps, and port-provenance: references/social-login.md.
Config shape (see references/config-scaffold.md for the full schema):
{ "socialLogin": {
"communityMemberProfile": "Customer Community User",
"authProviderNames": ["Google", "My_SAML_Provider"],
"communityUserPermset": "myapp_Guest_User_Api_Access"
} }
Sequence (mirrors reference org-setup.mjs main() social-login step, which runs
after self-registration and before the data/GraphQL steps):
Apply the assets/ templates verbatim (like the self-reg/data steps) — full
runnable commands for each sub-step are in references/social-login.md:
- Derive the site — run
scripts/derive-site-name.sh(single*.network-meta.xml; stop if zero/ambiguous). Social login needs the site to resolve itsAuthConfig. - Enable "Allow standard external profiles" — deploy
assets/Communities.settings-meta.xmlvia Metadata API (from a throwaway minimal project; see the reference). Required so the SSO registration handler can create users on standard community profiles; without it auth providers fail user insert withFIELD_INTEGRITY_EXCEPTION. A "already active" warning is a non-fatal skip. - Link Auth Providers to the site
AuthConfig— runassets/social-login-auth-providers.apexverbatim (substitute<siteName>,<ProviderNamesList>,<ApiVersion>) to create the missingAuthConfigProvidersjunctions, then read its|DEBUG|lines (table in the reference). All-or-nothing: if any configuredauthProviderNamesentry has no matchingAuthProvider(OAuth) orSamlSsoConfig(SAML) record, it emitsMISSING_PROVIDERSand links nothing — create/fix them in Setup first. Already-linked providers are skipped (idempotent). - Add the community member profile to
NetworkMemberGroup— resolve the Network + Profile Ids and create the membership if absent (sf datacommands in the reference). Without it, SSO-registered users hitNO_ACCESS: User was not authorized for the community. - (Optional) Assign
communityUserPermsetto community users — only when configured. GrantsApiEnabledsogetCurrentUser()(/chatter/users/me) works for SSO-created users.
IMPORTANT: A
socialLoginblock present but skipped is a silent-failure trap — the app deploys fine, but no social-login buttons ever appear on the login page and there is no error explaining why. Do not silently skip: run the step when the block is present, or state clearly that it was absent.
Step 7 — Data import (presence-driven) — ask first
Run scripts/find-data-plan.sh first. If it exits non-zero, skip this step —
do not prompt and do not error; just move on to step 8 (a brief "no data plan,
skipping data import" note is fine). There is nothing to import without a plan.
On success it prints the plan's path (it searches recursively, so both a
project-root data/ and a <packageDir>/main/default/data/ layout resolve).
When it exists: always ask the user before importing or cleaning data — it deletes existing records first. Apply the verbatim templates; do not improvise Apex:
- Run
scripts/find-prep-script.sh. If it succeeds, it prints the path of aprepare-import-unique-fields.jsthat ships with the app — run that first; it deduplicates re-runs by stamping stable unique keys on the record files. Invoke it the way that copy expects (its interface varies — seereferences/data-import.md). If it exits non-zero, there is no prep script — skip to the clean step. - Clean in reverse plan order (children before parents) with
assets/data-delete.apex. - Import in forward plan order with
assets/data-import.apex, resolving@referenceIdrefs and batching by measured size.
Protocol, @referenceId resolution, measured batching, and the
SETUP_RESULT_JSON parse-and-hard-fail rule: references/data-import.md.
Step 8 — GraphQL schema fetch + codegen
Run from the UI bundle directory, after deploy and permission assignment (the schema reflects org state and the caller's FLS):
npm install
SF_TARGET_ORG=<org> npm run graphql:schema
npm run graphql:codegen
npm run build
Detail: references/graphql.md. Re-run schema fetch + codegen after every deploy
that changes objects, fields, or permissions.
Done
Setup ends here — the steps above (including the config-gated 6b social login)
are the complete sequence. Local dev preview (npm run dev:preview) is a separate
developer action, not part of setup; if the user asks to preview the site, see
references/dev-preview.md.
Critical rules
- Deploy metadata before fetching schema — custom objects/fields appear only after deploy.
- Assign permissions before schema fetch — the caller may lack FLS otherwise.
- Re-run schema fetch + codegen after every metadata deploy that changes objects, fields, or permissions.
- Never silently skip permission assignment, self-registration, social login, or
data import — either the convention file/config block is present (run it, asking
first for the destructive ones) or it's absent (skip cleanly and say so). A
present-but-skipped
socialLoginblock means no login buttons appear with no error to explain it. - Discover the source path from
sfdx-project.json; never hardcodeforce-app/main/default. - Apply the
assets/Apex and XML templates verbatim — they encode duplicate-rule bypass,allOrNone=falsedeletes, idempotency, SOQL-safety, and (for social login) the all-or-nothing provider pre-check and the DML-not-allowed-on-AuthConfigProvidersREST-callout insert — all easy to get wrong by hand.
Interaction order (summary)
- Authenticate org
- Build UI bundles (pre-deploy)
- Deploy metadata (deploy-license gate if self-reg configured)
3b. Set the site logout URL (if
logoutUrlconfigured — post-deploy, idempotent, best-effort) - Assign permission sets (config-driven assignee)
- Assign role (if configured)
- Enable self-registration (if configured — ask first)
6b. Enable social login (if
socialLoginconfigured — idempotent, no ask needed) - Import data (if data plan exists — ask first)
- Fetch GraphQL schema + codegen + final build