Promotion
Promotion compares two environments, lets you choose the rows to move, and writes them into the target as drafts or live versions in one transaction, with a record of who did it.
Why promotion refuses what you did not see
Work prepared in staging has to become the content prod serves, exactly as it was reviewed and nothing more. If you copy documents across one by one, you lose track of what moved. A bulk copy is no better, because it overwrites whatever changed in prod in the meantime. Promotion reads the source and never writes to it, and it refuses to overwrite anything in the target that you did not see in the compare. When it refuses, it writes nothing at all, so a target is never left half-promoted.
Content managers reach it on the Promote tab. Engineers call the same two routes, compare and promote, from a script. Environments and promotion explains why the CMS leaves a conflict to a person.
What a compare shows and a promotion writes
Compare
A compare is addressed at the target, the environment a promotion would write into, and it names the source in from. Its scope is a list of up to 50 folders, each with the locales it covers, and an empty folder means everything. Every document under the scope comes back with one of four statuses:
| Status | Meaning |
|---|---|
added |
in the source, not in the target |
changed |
in both, with different bodies |
unchanged |
in both, with the same body |
removed |
in the target, gone from the source |
Each row carries both versions and both hashes. It says when only the metadata differs (metadataDiffers), and it lists referenced documents that the target lacks and the scope misses (missingRefs). Drafts count: a draft in the source is what would be copied, and a draft in the target is what would be overwritten. Folder settings and components are compared like any document. Files are not, because every environment shares them.
A compare changes nothing. Any member may run one, and a CLI key needs content:read.
Promote
A promotion names the rows to move. Each row carries, as its baseHash, the target hash the compare reported, which says "this is the version I looked at", or null, which says "there must be nothing here". If the target has moved since, the CMS refuses the whole call. A row whose source is gone becomes a deletion in the target, and the deleted document keeps its versions.
The CMS writes the rows in one transaction, folder settings first, then components, then everything else, so a component and the pages bound to it can travel in one promotion. Without publishing, the promoted versions are drafts in the target. With publishing, the CMS first checks each body against the target's published component. Then all the rows go live at one instant and the target's delivery cache is cleared, exactly as a batch publish does.
Each row comes back created, updated, unchanged, deleted or not-found. With publishing, publishStatus says published, or unchanged when that version was live already.
Promoting needs the editor role, or a CLI key with content:write. Publishing in the same step also needs content:publish.
The audit trail
The CMS records every promotion and every clone against the environment it wrote into: who ran it, when, from where, the scope, whether it published, and the counts. Each promoted version says where it came from, and History in the target shows "Promoted from staging (v2)".
Refusals
| Status | error |
When |
|---|---|---|
| 409 | base_mismatch |
A row changed in the target since the compare. conflicts names every one. |
| 409 | in_use |
A deletion of a document that live content in the target still references. usedBy names the referrers. |
| 409 | conflict |
Other writes kept winning the race three times over. Try again. |
| 403 | batch_forbidden |
Some rows are in folders this person or key may not change, such as a folder only the CLI may write. |
| 403 | forbidden |
The role or scope is missing, or the key is bound to one environment. |
| 422 | validation_failed |
A row is invalid: a missing component in the target, a document that is Markdown on one side and JSON on the other, a broken reference. The field is entries.N.…. |
| 413 | payload_too_large |
The request is over JSON_BODY_LIMIT. |
| 429 | rate_limited |
The promotion budget for this person or key is spent. |
A missing component reads No schema "hero" in prod; promote _schemas/hero with it. and a type clash reads This document is md in prod and json in staging. Delete it in prod first.
How to
Dashboard
- In the header, choose the environment to write into, then open the Promote tab. To is that environment. Choose the source in From. Both selects list every environment, so with two environments you are not stuck with one option each: choosing in From the environment To holds swaps the two, and the swap button between them does the same in one step.
- Under Folders, leave the one row empty for everything, or type a folder such as
landing/, and choose its locales. Add folder adds another row. - Press Compare. A line such as "3 added · 12 changed · 1 removed · 240 unchanged in prod, compared with staging just now." sits above the table. Unchanged rows stay hidden until you turn them on.
- Select rows. Show diff draws the two bodies side by side. The Select menu picks every added, changed or removed row at once. "Select all shown" never picks a removed one, so you always choose a deletion on purpose.
- Tick Publish after promoting to make the rows live in the same step, then press the button that counts your selection, such as Promote 14 documents.
- The confirmation names both environments, counts what will be added and changed, and names each deletion. Add a Message if you want one in History, and press Promote or Promote and publish.
A selection of more than 500 rows goes in chunks of 500, each its own transaction, with a progress bar. If the CMS refuses a chunk, the screen says which one, and the chunks before it stay promoted.
A refusal says "Nothing was promoted." and then shows its reason in the place it concerns. "Changed in prod since you compared" comes with Compare again, which keeps your selection on the rows that did not move. "Still in use in prod" names what references a deletion. Deselect them appears for folders the dashboard may not change.
Recent promotions into Prod, under the comparison, lists the last ten, and a clone there reads "Cloned from prod · 3 410 documents". Open one to see its rows. Viewers can compare and see the trail, but they cannot promote.
API
Use a project-wide CLI key or a personal token, sent as <keyId>:<secret>. Compare the source with the target named in the path:
curl -s -u '<keyId>:<secret>' -X POST \
'http://localhost:3000/api/v1/projects/<project>/envs/<target>/compare' \
-H 'Content-Type: application/json' \
-d '{"from":"<source>","scope":[{"prefix":""}],"limit":500}'summary counts each status over the whole scope. items holds up to limit rows. While nextCursor is not null, send it back as "cursor" to get the next page.
Promote the rows you chose. Each baseHash is that row's targetHash, and an added row has null:
curl -s -u '<keyId>:<secret>' -X POST \
'http://localhost:3000/api/v1/projects/<project>/envs/<target>/promote' \
-H 'Content-Type: application/json' \
-d '{"from":"<source>","publish":true,"entries":[
{"key":"<changed-key>","locale":"<locale>","baseHash":"<targetHash>"},
{"key":"<added-key>","locale":"<locale>","baseHash":null}]}'The answer names the record in promotion and each row's outcome in results. To write drafts instead, leave out "publish":true. Read the trail, newest first:
curl -s -u '<keyId>:<secret>' \
'http://localhost:3000/api/v1/projects/<project>/envs/<target>/promotions?limit=10'And one record with its rows:
curl -s -u '<keyId>:<secret>' \
'http://localhost:3000/api/v1/projects/<project>/envs/<target>/promotions/<promotion>'Limits
- There is no CLI command.
quillet promoteis designed but not built, so promote from the dashboard or over the API. - There is no scheduling, no approval step and no named release. A promotion happens when somebody runs it.
- There is no three-way merge. A conflicting edit is a 409
base_mismatch, which a person resolves by comparing again. - An edit in the source between your compare and your promotion is not a conflict, because the promotion copies what the source holds when it runs. To refuse that case too, send each row's
sourceHashfrom the compare. The dashboard does not. - A promotion that publishes cleans each body under the target's published component. The next compare of the same pair can therefore read
changedfor a document nobody touched. - A compare or a clone covers at most 20 000 documents on either side. A compare page holds at most 500 rows, a promotion at most 500 rows, and a scope at most 50 folders.
- Compare, promote and clone share one budget per person or key, 100 a minute by default (
PROMOTION_RATE_LIMIT). Compare again drains the whole report afresh. - A key bound to one environment can neither compare nor promote, because both read a second environment.
- Files are never promoted. They belong to the project.
Related
- Environments and promotion: what a compare classifies and why.
- Environments: cloning, the other way an environment gets content.
- Publishing and history: a batch publish, which a promotion with publishing behaves like.
- Management API: every route, its errors and
If-Match. - Limits and quotas: the promotion budget and the batch sizes.