Skip to content

cwp push

shipped 1.0.0
cwp push [env] [flags]

Acts on the environment the project makes obvious: the only one it defines, or the one called dev. Name another to act there. A project with several and no dev has to name one.

tree → site

ArgumentWhat it isDefault
[env]environment to write to (default: default_environment in cwp.yml)
FlagWhat it doesDefault
--only <what>narrow to one artifact, or to code for the tracked paths
--forceoverride the refusals a crossing makesoff
--no-backupskip the remote backup taken before the writeon
--deletemirror the tracked paths: also remove remote files that are absent locallyoff
--prunewith —only roles or —only inventory: delete what the file does not listoff
--exactwith —only inventory: pin to the recorded versionsoff
--replacewith —only bricks: overwrite items that already exist instead of skipping themoff
--sensitivedoes nothing since 2.0: credentials never enter the tree, and customCss travels on its ownoff
--skip-in-usewith —only bricks: leave items the site still references in placeoff
--skip-incomplete-fontswith —only bricks: push everything except fonts the tree carries no faces foroff
--keep-superseded-fontswith —only bricks: leave the font files this import replaces in the media libraryoff
--no-snapshotwith —only bricks: skip the local design-system export taken before a replaceon
--overwrite-conflictswith —only content: overwrite posts that changed on the target since cwp last saw themoff
--media <mode>with —only content: attachments the target lacks, upload | require
--publishwith —only content: a post the target does not have yet arrives publishedoff
--status <status>with —only content: set an existing post’s status too
--delete-termswith —only content: remove terms of a declared taxonomy the tree no longer describesoff
--skip-missing-targetswith —only content: drop a menu entry whose target is absent instead of refusingoff
--yesskip the confirmation promptoff
--with-agentwith —dry-run on a host with no shell: install and remove the PHP agent so the plan is realoff

Plus the shared flags --json, -v, --verbose, -q, --quiet and --dry-run.

What it does

cwp push makes an environment match the committed tree. It carries both halves of what is under management:

  • the six artifacts — content, the Bricks design system, settings, widget areas, roles and the inventory, the same six cwp pull reads down. Each artifact’s page holds its own rules, under the older spelling that stays as an alias: content, bricks, settings, widgets, roles, inventory;
  • the tracked paths from cwp.yml: your theme and plugin files. They are code and have no baseline. An operator shipping a change means them as much as the content.

One command carries all of it for a reason. A partial deploy leaves an environment that matches no commit. The provenance record exists to detect that state. One push is one confirmation and one remote backup for what you experience as shipping one change.

--only <what> narrows a run to one of the six, or to code for the tracked paths alone. cwp refuses a name this project does not carry rather than doing nothing.

--only code ships your own theme or plugin and touches nothing else: a hotfix, or the code a page needs before the page can arrive.

The order it sends things in

A supplier goes before what uses it, and the code goes before all of them:

tracked paths → inventory → bricks → content → settings → widgets → roles

A page can point at an element type your plugin registers or at a global class the design system carries. Neither points back at a page. So the run sends them first and writes the pages onto a site that already has what they need.

The reference check reads the environment as it is before the run. A class or element type that this run is about to deliver gets a warning rather than a refusal:

! 4 element types not on "dev" yet: acme/hero, … — they arrive earlier in this
  run, and the pages render wrong if they do not

A reference nothing in the run supplies still refuses. The check exists for that case: a page saved with a dangling reference renders wrong rather than failing.

A deploy’s post-steps come last. The cache flush, the provenance record and post-deploy.sh describe the run as a whole, so they run after the content and not after the files.

One plan, one decision, one restore point

Each artifact’s survey produces a sub-plan, and cwp concatenates their effects into one plan. The guard ladder reads every remote write this run will make and decides once:

  1. Resolve where it goes. The environment you named, or the one default_environment: declares. A project that declares none refuses rather than picking: cwp does not guess which site a write is for.

  2. Refuse an environment that was never adopted. cwp adopt gives the tree its relationship with an environment.

  3. Refuse if the environment moved since cwp last transferred (below).

  4. Refuse if the environment is protected and --force was not given: exit 5. 3a. Refuse if the environment is in launch or live and a readiness check fails there: the sandbox captcha key, the mail guard on the environment, the scrub’s placeholder accounts. No flag passes it; the line names the fix, or stage: build while the site is not live (stage).

    ✗ "prod" is live and turnstile site key on prod is the sandbox key, and every submission passes
      → cwp edge captcha prod writes the zone's widget into the builder, or set stage: build while prod is not live
  5. Show what would move and ask for confirmation, unless --yes. Exit 6 when cwp cannot prompt.

    Each artifact says it in its own words and names the destructive parts. The posts it would overwrite, and which of them somebody changed on the site. The design-system items it would delete. The capabilities a role would lose. The plugins a --prune would remove. --dry-run prints the same list. Read it before you decide.

  6. Run pre-push.sh. A non-zero exit aborts.

  7. Take one remote backup, unless --no-backup.

  8. Write, then verify: per file for the tracked paths, per item for content.

Nothing happens until the confirmation passes: a refused push takes no backup and does not run your hook. One plan means one restore point.

Verification compares a digest per file rather than counting files. A transfer tool decides what to send by size and modification time. A file on the site that differs in content alone reads as up to date. The tool never resends it. Counting files would never see it.

Where the transport has a flag to stop trusting size and time, cwp warns that it is re-sending that path on content and sends it. When the second pass matches, the run reports a repair step. It runs once. A second mismatch is a fact about the host: no room, no permission, something else writing the file. Cloudron has no such flag, so the run names what to do by hand.

cwp deploy is this command plus the remote post-steps and the post-deploy hook. They share one implementation, so their flags and guards cannot drift apart.

It refuses when the environment moved

git push refuses a non-fast-forward, and so does this. The survey reads each artifact from the environment and compares it against the baseline the last transfer agreed on. If the two disagree, the push stops and names what does:

The command the line names is not held up by the line: `cwp edge captcha
prod` writes the door the check reads, so the refusal lets it through.

✗ "prod" no longer matches what cwp last recorded for it: roles, settings
  → somebody changed it there, or cwp's last transfer did not fully land — read
    it down first (`cwp pull prod`) and commit what you want to keep, or pass
    --force to overwrite what is there

It names both readings because one comparison cannot tell them apart. An artifact that did not fully converge records no baseline. That makes the second reading rare. A baseline written by an older cwp is still in the file.

It sees more than a count would. A role renamed, a capability granted, a plugin deactivated at the same version, a widget moved within its area: the survey compares each of them.

Content gets a finer check. Its own push refuses per item when something changed on both sides. --overwrite-conflicts is the deliberate override.

One artifact has no check, and the run says so:

! not checked for changes on "prod": bricks — these artifacts cannot yet report
  what the environment holds, so a change made there would be overwritten
  without warning

The design-system push uploads a package and lets the builder merge it. It never reads what the site holds, so there is nothing to compare. The run says so rather than leaving you to assume otherwise.

It creates the accounts the content needs

An item carries the login that wrote it. A push resolves every one of them on the target before it writes anything. It creates the missing accounts from content/authors.yml: login, display name, address, roles, and a random password nobody ever sees. cwp sends no mail. The person gets in through the site’s own password reset.

cwp never changes an account that is already there. It reports a display name or an address that differs from the tree and leaves it alone: a push may not move somebody’s login credential. The next cwp pull brings the tree in line.

A login the target lacks and the tree does not describe stops the run before cwp writes anything. The refusal names all of them at once:

✗ 2 author(s) cannot be resolved on "prod":
    naima — content/authors.yml does not describe this person
    arno — content/authors.yml carries no email address for them
  → pull the people into the tree first — `cwp content pull` writes
    content/authors.yml — or create the accounts there

Set author on an environment and every item goes there under that one login instead. The tree still holds who wrote what.

The flags that belong to one artifact

--prune, --exact and --delete are not general. Each answers a question only some artifacts have. Each is legal only where it means something. Elsewhere the command refuses it rather than doing nothing:

FlagWhere it applies
--prune--only roles, --only inventory — delete what the file does not list
--exact--only inventory — install the recorded version over a different one
--replace--only bricks — overwrite items that already exist instead of skipping
--sensitive--only bricks — include the api-keys and custom-code tabs
--skip-in-use--only bricks — leave items the site still references in place
--skip-incomplete-fonts--only bricks — push everything but fonts with no faces
--keep-superseded-fonts--only bricks — leave replaced font files in the library
--no-snapshot--only bricks — skip the local export taken before a replace
--overwrite-conflicts--only content — write over a post that changed on the target
--media <mode>--only content — upload attachments the target lacks, or require them
--publish--only content — a post the target lacks arrives published
--status <status>--only content — set an existing post’s status too
--delete-terms--only content — remove terms the tree no longer describes
--skip-missing-targets--only content — drop a menu entry with no target
--deletean un-narrowed run or --only code — mirror the tracked paths
✗ --exact does not apply to `roles`
  → use it with the artifact it belongs to: cwp push --only inventory --exact

Two flags are absent on purpose. There is no selection: no --post, --type or --all. A crossing carries everything under management, and narrowing to a few items leaves a baseline that means nothing in particular. --only <artifact> is the one narrowing there is.

There is no --no-verify either. A crossing either converged or it did not, and verification turns “exited zero” into “the site holds what the tree describes”. The flag stays on cwp content push, where skipping the read back on a single artifact is a diagnosis rather than a habit.

Read --delete twice: it mirrors the files, and a run narrowed with --only does not carry them. An artifact’s own deletions have their own flag.

Additive by default, mirror with --delete

cloudron sync push only ever adds and overwrites. A file you delete locally stays on the remote. In a plugin directory WordPress keeps loading it, so a file you think you removed can still be running. The default stays that way: deleting live files as a side effect of a routine deploy is the surprise this tool exists to prevent.

That paragraph is about the tracked paths. cwp converges the six artifacts item by item under their own rules. Their pages describe those rules.

The default is not silent about it. When the remote holds files your tracked path does not, cwp says so and names the count. --delete mirrors instead, and it sits behind every guard above. Protected environments still refuse. The confirmation still runs and says outright which files it will delete. cwp still takes the backup first. cwp push --delete --dry-run lists the exact files it would remove and changes nothing.

Verification, and why it is per file

After the transfer, cwp compares an md5sum of every remote file against your local tree. A file that never arrived, or arrived with different contents, fails the push. The error names it.

cloudron sync decides what to send by size and mtime. A remote file corrupted to the same length with its mtime intact reads as Already up to date. The transfer never resends it. Re-running the push does not repair it. Touch the local file to force a resend, or delete the remote copy. The error says so.

If the remote image has no md5sum, cwp warns and falls back to comparing file counts. That is weaker, but it does not fail a good push.

What the dry run can and cannot tell you

The plan is the tracked paths, their local file counts and their remote targets. On the Cloudron provider it is not a file-level diff. Cloudron has no transfer dry run, so only a transfer shows what differs. The dry run says that in as many words. On the ssh provider it is a file-level diff, because rsync has one.

With --delete the dry run lists the deletion set exactly. A remote listing is enough to compute it.

Files on the far side that are not yours

A transfer made by hand from macOS leaves a ._file beside every real one. cloudron push packs a directory with the BSD tar of the machine it runs on, and GNU tar on the far side unpacks the extended attributes as separate files. On every transfer cwp makes itself, it sets COPYFILE_DISABLE=1. That is no help for a transfer you make yourself, so cwp names the files instead:

! 36 AppleDouble file(s) under /app/data/wp-content/plugins/site on dev
  (._site.php, ._hooks.php, …) — macOS `tar` writes them when a directory is
  transferred by hand. WordPress loads none of them; remove them with
  `cwp deploy dev --delete`, which mirrors

They are rubbish rather than a danger. WordPress loads none of them, because glob('*.php') matches a leading dot only when the pattern writes one. Removing them takes --delete: cwp does not delete remote files without it.

What an artifact could not do

An artifact converges what it can and reports what it cannot. A plugin cwp could not fetch. A role the site would not take a capability from. A page the target stored differently from what cwp sent. The run names each one under its artifact and says how many did not land:

! inventory — 0 item(s) converged, 1 did not
    my-theme 2.4 (source: media/my-theme.zip) — the far side could not resolve it

An artifact that did not fully converge records no baseline. The next push therefore does not accuse the environment of having moved: cwp writes down what happened, not what it intended. --json carries the same lines under unfinished, keyed by artifact.

--json records which guards ran

Every push and deploy carries a guards object. It says whether the protected-environment refusal applied, whether cwp took or waived a restore point, and whether the change had a committed source:

{ "guards": { "protectedEnvironment": "passed-with-force",
              "restorePoint": "taken",
              "source": "present" } }

A guard that silently did not run shows as a missing line there.

Example

cwp push dev --dry-run           # see what would be sent
cwp push dev --delete --dry-run  # see what would be sent *and removed*
cwp push prod --force            # prod is protected; --force is required

What lands before what

A full push sends the tracked paths first. Then come the inventory, the design system, the content, the settings, the widgets and the roles: a supplier before what uses it. Settings come after the content because the options holding a post id need the post to be there.

A settings group marked before: content in cwp.yml is the exception. It holds the options a plugin registers post types, taxonomies or fields from. That half of the settings lands before the content, and the run shows it as settings (schema). The rest of the settings stays after the content.

What the target needs afterwards

A push writes data past the forms a plugin would have used. What the plugin does when its own form saves, it does not do after a push. The transient it holds a table in stays stale. The counter row it updates with a bare UPDATE never exists. The rewrite rule for a sitemap is not there until somebody flushes.

A family names those steps (settle in its file, see the cwp.yml reference). The push runs them after the artifacts and before the ref moves. Each step is an effect in the plan: the dry run lists it and the confirmation names it. A step that fails is a push that did not arrive. Two families naming the same step get one. A flush of the rewrite rules runs last.

  settle example-child/redirects: delete transient ex_all_redirects
  settle example-child/redirects: redirect_clicks = 0 on every ex_redirect without one
  settle example-child/seo: flush rewrite rules

A run in which no artifact writes settles nothing. What only a project knows stays in its post-deploy hook.

The page builder has steps of its own. On a site loading its CSS from files, a design-system push that carried classes, variables, theme styles or components writes Bricks’ per-post CSS again. After a content push onto a site with query filters on, cwp rebuilds the filter index through bricks/reindex-filters. A target that gained the filter tables after its content had arrived had empty ones until then. The run names both steps.

What it does not do

It does not push the database. cwp has no upward database command, and cwp deploy is not one. The project deferred a whole-database push; Safety records that decision and why. This command carries the tree and the tracked files: the things that have a reviewed, committed source.

It does not push media. The files an attachment points at travel downward in the tree and never go back up.

It does not push anything outside the tracked list, run hooks during a dry run, or delete anything remotely unless you pass --delete.

It does not merge. Like the pull, it refuses and hands the decision back to you, with git diff as the surface you make it on.