leed site validate, commit and push

These three commands are always run in this order, and each refuses to do its job until the one before it has done its own. This page is the reference: every flag, every gate in the order it runs, and the exact sentence each refusal prints. The workflow itself — why it exists, what it produces, and how it is enforced — is on validate, commit and push.

Run all three from the project folder or from raw-content/.

leed site validate

leed site validate
leed site validate --reset --dry-run
leed site validate --reset
FlagTypeDefaultRequiredWhat it does
-r, --resetbooleanfalsenoReturns every restricted file to the state the site template ships it in
--dry-runbooleanfalsenoWith -r, lists what would be reset and changes nothing
-y, --yesbooleanfalsenoConfirms --reset without being asked. Required to run it unattended

Validation runs its checks in a fixed order, and the order is what decides which message you get:

  1. The confirmation gate, for a real --reset only. See the warning below.
  2. Is there anything to do. Nothing modified is a refusal, not a success: “No files were modified. Nothing to do.”, exit 1.
  3. The reset, when -r was passed. With --dry-run it lists what it would do and stops there, exiting 0 without validating anything.
  4. The restricted-file check. Offending files are listed with the rules they broke, followed by “Run the following command to automatically remove and reset any restricted files:” and the leed site validate --reset line. Exit 1.
  5. Per-file validation, which records each file’s modification time.
  6. The tracking file is written and validation confirms: “All changed successfully validated. Please build and visually inspect your site.”

Validation is per file and per edit. It records the modification time of everything it passed, so touching a file again makes it unvalidated again and leed site commit will say so.

The rules

RuleWhat it coversEffect
Read-only, restored on sightidentity.json, .gitignoreReset before any other check runs, so they never even appear as changes
Protected pathssrc/_data/**/*Blocked — this is the data the CMS writes
Page-type folderssrc/<page-type-slug>/**/*Blocked. Built at run time from src/_data/pageTypeList.json, so the protected set grows the moment you create a page type
Disallowed patterns**/*.11tydata.json, src/static/images/favicon-*, src/static/images/logo-*, README.md, bunfig.toml, .ci/**/*Blocked
.hbs exemptionA file matching a path or pattern rule above whose extension is hbsNot blocked — templates are yours even inside a protected folder
Disallowed extensionsMedia, office documents, archives and three font formatsBlocked. Full list below
Maximum file size500 KB, at or aboveBlocked. Override with FILE_VALIDATION_MAX_SIZE, which is read in kilobytes
The full disallowed-extension list

mov, mpg, avi, mp4, mp3, wav, pdf, xls, xlsx, doc, docx, ppt, pptx, zip, ttf, ps, otf

The three font entries are ttf, otf and ps. There is no allowlist and no WOFF2 rule: woff and woff2 are simply not on this list, so both commit normally.

Video, audio and PDF are on it because they belong in the asset library, where they are served from object storage and get a delivery URL — not in a git repository that is cloned on every build.

There is no frontmatter validation. Per-file validation currently records the file and passes it; the rules that actually block a commit are the path, pattern, extension and size rules above.

The same rules stated as repository policy, rather than as command output, are in file validation rules and editable and read-only files.

Reset semantics

--reset collects everything the four rules rejected — restricted paths, disallowed patterns, disallowed extensions and oversized files — and acts on each according to what git says about it:

Git status of the fileWhat reset does
Modifiedgit restore — your edits are discarded
Deletedgit restore — the file comes back
Created and stagedgit rm — unstaged and removed
UntrackedDeleted from disk

Each file is printed with the action taken beside it, so the output is a record of what happened.

Because it is destructive, a real --reset now asks before it runs, defaulting to No:

Reset every protected file to its template state? Your changes to those files are discarded and cannot be recovered.

There are three ways that question is answered without a person:

  • --yes proceeds. This is the explicit, greppable way to run it unattended.
  • Under --json, or with LEED_JSON set, it is refused with the code confirmation_required and leed site validate --reset --yes as the hint. Nothing is touched.
  • In human mode with no interactive terminal — a pipe, a cron job, a CI step — it is also refused, with the code interactive_input_required and the same remedy.

--reset --dry-run is never gated: it exists to be run before deciding.

Where the tracking lives

Validation state is kept outside the git tree, at:

~/.cache/leed/<environment>/<companyId>/leed-validation.json

Outside deliberately — a stray git add in the content repository could otherwise commit your local workflow state. The directory is created mode 0700. Both the environment and the company id come from leed.config.json, and there is no fallback for either: run one of these commands outside an initialized site directory and it refuses rather than operating without company context. Keying by company matters if you belong to more than one, since the same machine then holds several independent sets of state.

The file holds four things: lastSuccessfulBuild (the timestamp commit gates on), lastTouched (the newest validated edit), commitSet (the files, commits and per-file change kinds waiting to be reported to the CMS) and tracking (per-file modification times).

leed site commit

leed site commit -m "update the pricing page layout"
leed site commit -m "…" --dry-run
FlagTypeDefaultRequiredWhat it does
-m, --msg <string>string—yesYour commit message. Commander refuses the command without it
--dry-runbooleanfalsenoLists the files that would be committed, then exits 0

There is no git add. The CLI stages everything with git add --all after its gates pass, which is why the workflow is validate → build → commit rather than a staging dance.

The gates, in order

#GateMessage, verbatimFix
1Git identityRun the following commands to complete your setup:Run the two printed git config --global commands
2BranchYou are only allowed on specific branches. Change back to staging!git checkout staging
3Anything modifiedNo files were modified. Nothing to do.Make a change
4Restricted filesRun the following command to automatically remove and reset any restricted files:leed site validate --reset
5Everything validatedYou must run the following command before changes can be committed:leed site validate — the files needing it are listed underneath
6A build newer than your newest editYou must run the following command before changes can committed:leed site build

Every one of these exits 1. Gate 6’s message is missing a word — it reads “before changes can committed” in the shipped CLI. It is quoted here exactly because that is the string people paste into search.

Gate 6 is the one that traps people, because a --debug build does not satisfy it. The rule for which builds count is on leed site build.

Past the gates, --dry-run prints “These files have been modified and will committed:” followed by the file list, and exits 0. A real run stages, commits, and confirms:

	Changes were committed. You may now push your changes:

		leed site push

A staging failure prints “An error occurred while adding files:” with git’s own words beneath it; a commit failure prints git’s message directly, or “An error occurred while committing:” when the CLI’s own hook was what refused.

What your commit message actually looks like

The message git stores is not the sentence you typed. It is a prefix followed by JSON:

@leed/site-management=>{"message":"update the pricing page layout","userId":"k7q2x9","version":"4.105.0"}

Your text is the message field. userId is what lets the CMS attribute the change to you on the deployment row, and version is the CLI release that produced it, so a build failure can be traced back to a release. The commit-msg hook parses this and rejects any commit that does not carry all three fields — which is what stops a hand-written git commit from slipping through.

Git is invoked as git -c leed.invoked=true, a flag the pre-commit and pre-push hooks check for. That flag, set only by the CLI for the duration of one command, is the whole enforcement mechanism.

leed site push

leed site push

No flags. Push does nine things in this order:

#StepOn failure
1Check the git identityRun the following commands to complete your setup:
2Check the branch is stagingYou are only allowed on specific branches. Change back to staging!
3Count commits ahead of origin/stagingZero is a success: “No changes have been committed. Nothing to push.”, exit 0
4Revalidate the session against the serverDrops into an interactive device login; a refusal ends the command
5git fetch origin stagingCould not fetch from origin: with git’s words. If git’s words read as a credential refusal, the hint is leed auth rotate
6Rebase onto the remote, if it has movedThe rebase is aborted and the recovery printed — see below
7git push origin staging --no-verifyAn error occurred while pushing: with git’s error: and hint: lines
8Re-point the tracked commit id at the post-rebase HEAD—
9Notify the CMS in-processThe CMS was not notified: after the push confirmation — see below

Step 4 is unconditional. Push does not trust the local expiry stamp — it round-trips the refresh endpoint on every run, so a session revoked from another machine is caught here rather than at step 9. The reason is step 9 itself: a push whose token dies partway would put commits on the remote and never tell the CMS, and that notification is not replayed automatically. CLI authentication covers the session model behind it.

Rebase, never merge

The content repository takes commits from more than one place — the CMS writes published pages and data files into it, and your teammates push templates. When the remote has moved, push replays your commits on top of it rather than merging, so history stays linear and every deployment maps to one commit.

A successful rebase says so before the push confirmation:

	Rebased 3 remote commit(s) onto your work.

	Site pushed successfully...

	CMS successfully notified!

A genuine conflict aborts the rebase — leaving your working tree exactly as it was — and prints the recovery:

	Remote has conflicting changes that can't be auto-rebased.
	Run `git pull --rebase` in the content repo, resolve conflicts, then retry `leed site push`.
cd raw-content
git pull --rebase
# resolve the conflicts, then:
git add <the files you fixed>
git rebase --continue
cd ..
leed site push

Step 7 passes --no-verify, which skips the pre-push hook. That is intentional rather than a loophole: the CLI has already run every check the hook exists to enforce, and the hook would otherwise refuse the CLI’s own push. It is worth knowing so that seeing --no-verify in a trace does not read as something bypassing a safety net.

Step 8 exists because step 6 can rewrite your commit hashes. The commit id recorded at commit time would then name a commit that is not on the remote, and the CMS would trigger a build for a hash its clone cannot find. Re-pointing it at the post-rebase HEAD is what keeps the deployment matched to a commit that exists.

When the push works and the notification does not

Steps 7 and 9 can disagree. If the commits reach the remote and the CMS cannot be told, you get both facts, in that order:

	Site pushed successfully...

	The CMS was not notified:
		An error while connecting to server:
		TimeoutError: Request timed out: POST https://app.leed.ai/api/site/pushed

Your work is on origin/staging. What is missing is the deployment record and the build. The fix is to re-run the notification, not the push — pushing again finds nothing ahead of the remote and exits 0 without ever reaching step 9:

leed site notify-cms

leed site notify-cms

Hidden from --help, and normally reached only as step 9 of a push. It is documented here because it is the recovery for the failure above.

leed site notify-cms [--timeout <seconds>] [--retry <number>]
FlagTypeDefaultRequiredWhat it does
--timeout <number>number5noSeconds to wait for the request
--retry <number>number3noMaximum retries

It posts the recorded commit set and the current branch to /api/site/pushed, which is what creates the deployment record and launches the build workflow. Retries happen on 500, 502, 503 and 504 only, with a fixed three-second delay — a 401 or a 403 is answered once and reported, because retrying cannot fix either. On a 200 it clears the local tracking, so the next push starts from a clean commit set.

Its exit codes are its own, and they are the reason it is worth knowing about in a script:

ExitMeaningMessage
0Nothing to send — no commit set since the last notificationNo changes detected. Nothing to push.
0The CMS accepted the change setCMS successfully notified!
1A commit set exists but carries no HEAD commitNo HEAD commit recorded. Run ‘leed site commit’ first.
2No environment in leed.config.json, or no credentials for itMissing environment in leed.config. Run: leed site init / Not authenticated for. Run: leed auth login --env
8The request could not be completed at allAn error while connecting to server: with the underlying error beneath
9The CMS answered with a non-200An error occurred while sending changes:.

A 401 inside exit 9 is reported as an expired session, with the login command as the hint; a 403 is reported as a permission problem, with the hint to ask an administrator of your organization to grant this account permission to push site content.

When notify-cms reports “No changes detected. Nothing to push.” after a push has already gone out, that is the awkward case: your commits are on the remote and no deployment was created for them, because the tracking was already cleared. The way forward is a fresh commit — a whitespace change is enough — validated, built, committed and pushed, which carries the earlier work along with it.

Every code either command can return is listed in CLI troubleshooting and exit codes, and once the notification succeeds the CMS takes over: what happens after you push. These three commands, and every other one with its flags, are indexed at the CLI command reference.

ESC