Promote gates
Promote gates are the refusals a promotion, a build dispatch, a rollback or a release cut can return. Reach for this page when a request came back with a code and you need its meaning and its fix.
How it works
Every gate runs before anything is dispatched, metered or written. A refusal spends nothing and moves nothing: the channel stays where it was.
The refusals group by what they check. Request gates read the body. Channel, environment and build index gates read the project's records. Workflow gates read the mirror. Rollback and cut gates belong to those two endpoints alone. The operator gate and the quota gates read the workspace.
Access refusals are decided at the door, before any gate on this page runs. They apply to every endpoint here and sit in the last table.
Not every refusal carries a code. The refusals with no code come back with a message alone, so a client that switches on code must handle that shape too.
Reference
Request gates
| Code | Status | Meaning | Fix |
|---|---|---|---|
INVALID_BODY | 400 | The body does not match the endpoint's schema. details names the failed fields. GET /api/projects/{id}/releases/promotion-runs answers the same code without ids, with the message Missing dispatch ids to correlate. | Send the fields the endpoint documents. For a promote, buildId is the only required field. See the API reference |
INVALID_ORIGIN | 400 | The dispatch body named an origin the platform does not know. The accepted values are mcp, code, console, cli, cli-unknown, import and api | Send one of those seven, or leave origin out. A dispatch with no origin is recorded as console |
Channel gates
| Code | Status | Meaning | Fix |
|---|---|---|---|
UNKNOWN_CHANNEL | 404 | The promotion names a channel this project does not declare. The message lists the declared channels, which always include stable and preview. A release cut answers the same code when it names an undeclared channel | Create the channel under Project settings, Release channels, or promote to a channel in the list. See Channels |
UNKNOWN_SOURCE_CHANNEL | 404 | The promotion names a sourceChannel this project does not declare. canary is accepted as the legacy name of preview | Promote from a channel in the list, or leave sourceChannel unset. A promote to preview sources from preview; any other channel sources from itself |
CHANNEL_STATE_INDETERMINATE | 409 | The channel list could not be read at decision time, so the gate cannot tell whether the channel exists. A cut answers the same code | Retry |
A channel is promotable one second after it is created, because this gate reads the same channel record the console writes.
Environment gates
| Code | Status | Meaning | Fix |
|---|---|---|---|
BUILD_ENV_MISMATCH | 409 | The build's stamped environment does not match the environment bound to the target channel | Rebuild the commit in the channel's environment, or promote to a channel bound to the build's environment |
UNSTAMPED_BUILD_BLOCKED | 409 | The build carries no environment stamp and the project's environments are populated, so the gate cannot place it | Rebuild the commit. New builds are stamped |
ENV_STATE_INDETERMINATE | 409 | The environment state could not be read at decision time | Retry. If it persists, check Environments |
Build index gates
| Code | Status | Meaning | Fix |
|---|---|---|---|
UNKNOWN_BUILD | 404 | The sha is not in the build index. The platform never produced that build | Build the commit first, or correct the buildId |
REBUILD_REPLACES_LIVE_ARTIFACT | 409 | The rebuild would overwrite artifacts a live channel serves, for an environment other than the one that channel requires | Rebuild with the environment the channel requires. A person in the console can overwrite anyway with Replace and rebuild, which sends force: true; a project token is refused 403 REBUILD_FORCE_NEEDS_A_PERSON for it, so its message never offers it. A production rebuild of a commit that only preview serves passes without force |
Workflow gates
| Code | Status | Meaning | Fix |
|---|---|---|---|
RELEASE_WORKFLOW_NOT_FOUND | 404 | Promote, rollback and GET /api/projects/{id}/releases/promotion-runs read the project's release workflow, and the mirror carries none | Reconnect the project under Project settings, GitHub integration, which restores the mirror's workflows |
RELEASE_DISPATCH_FAILED | 500 | One promote run dispatches per browser, and every one failed | Retry. A promote where at least one browser dispatched answers 200 and names the browsers that did not |
Rollback gates
| Code | Status | Meaning | Fix |
|---|---|---|---|
NO_ROLLBACK_TARGET | 409 | The channel's latest promotion left no earlier build: a first promotion, a promotion of the build the channel already held, or a channel with no event log | Promote a build by id with POST /api/projects/{id}/releases. The channel gains a target once a second, different build enters it |
ROLLBACK_HISTORY_UNREADABLE | 503 | The promotion history could not be read. A timeout, a registry error, a rejected token and a non-JSON answer all read this way | Retry. If it persists, the registry is the thing to check |
ROLLBACK_DISPATCH_FAILED | 502 | A target resolved and every browser's dispatch failed | Retry. A run where some browsers dispatched answers 200 and names the ones that did not |
Release cut gates
A cut bumps the manifest version in your source repository and opens a release PR, so its refusals are about that repository.
| Code | Status | Meaning | Fix |
|---|---|---|---|
NO_SOURCE_REPO | 409 | The project is not connected to a source repository | Connect one under Project settings, GitHub integration |
APP_NOT_INSTALLED | 409 | The extension.dev GitHub App is not installed on the source repository | Reconnect the project, which reinstalls the App |
MANIFEST_NOT_FOUND | 409 | No manifest.json with a version was found. The cut looks in the project's root directory first, then in the usual locations | Bump the version by hand and push, or set the root directory to the folder that holds the manifest |
VERSION_INVALID | 400 | The requested version is not one to four dot separated integers from 0 to 65535, such as 0.3.0 or 1.2.3.4 | Send a version in that shape, or send a level of patch, minor or major instead |
VERSION_NOT_AHEAD | 400 | The requested version is not greater than the one the manifest carries. The message names the current one | Ask for a higher version |
VERSION_UNPARSEABLE | 409 | The manifest's current version could not be read as a version | Name the version you want with version, or correct the manifest and push |
SOURCE_REPO_UNREACHABLE | 409 | The source repository answered 404 through the GitHub App. Renamed, deleted and uncovered by the installation all read this way | Reconnect the project under Project settings, GitHub integration |
Operator gate
| Code | Status | Meaning | Fix |
|---|---|---|---|
BUILD_DISPATCH_DISABLED | 429 | An operator paused build dispatch. Every request that dispatches a run on your mirror answers this code while the pause holds | None on the client. Wait for the operator to lift the pause |
The pause holds the console Rebuild button, POST /builds/dispatch, a promote, a rollback, a release cut, a store submission from the console, the API or the CLI, a rollout raise, a submission cancel and a store credential recheck.
The lanes with nobody waiting hold too. The push webhook answers its delivery with the code DISPATCH_DISABLED and skips the build. The dispatch sweep records a kill_switch refusal. The store-status sweep returns paused. A merged release PR stays pending and builds after the pause lifts. The first build after an import is skipped with a line in the import stream, and the project builds from the Builds page once dispatch resumes.
Refusals with no code
| Status and message | Endpoint and cause |
|---|---|
400 No commits found on the target branch. | Dispatch. The commitSha was not found, or the branch carries no commits |
400 GitHub App installation not found for this project. | Dispatch. No App installation is recorded. Reconnect the project |
404 No build workflow found for this project. | Dispatch. The mirror carries no build workflow. Reconnect the project |
405 Method not allowed, with an Allow header | Any endpoint here, reached with the wrong method |
500 Internal server error | Any endpoint here, when the handler threw. Retry, and read the run history before promoting again |
Quota reasons
Every quota refusal names one of twelve reasons. Where the answer lands depends on the endpoint, so read this table with the door codes below it.
| Reason | Meaning | Fix |
|---|---|---|
monthly_cap | The monthly build allowance is spent | Wait for the monthly reset, or raise the plan at extension.dev/pricing |
burst_cap | Too many push builds inside a sliding 60 minute window: 12 on free, 30 on pro. Decided on the push webhook and recorded, not answered | Wait. The window slides and capacity returns as older dispatches age out |
kill_switch | An operator paused dispatch. This is the reason behind BUILD_DISPATCH_DISABLED | None on the client |
push_builds_exhausted | The workspace spent its push builds for the month: 25 on free, 350 on pro. Only a git push on a branch the project builds is counted | Click Rebuild in the console to run the same commit now. The count resets next month. See CI and releases |
counter_unavailable | The usage counter could not be read, so the gate refuses rather than dispatches unmetered | Retry |
entitlement_unavailable | The workspace's plan could not be read, so no ceiling could be established | Retry. This is never a statement about your allowance |
unattributed | The push could not be tied to a workspace, so nobody could be billed. Decided on the push webhook and recorded, never answered | None on the client. The fault is in the platform's record of the project |
fork_pull_request | The commit sits on a pull request whose head branch lives in another repository. A build runs that code and spends your allowance, so a fork PR does not start one | Read the change, then click Rebuild in the console to run that commit yourself, or merge the PR, which builds it as a push |
macos_runner_not_included | The plan carries no Safari lane. Safari builds and App Store submissions run on a macOS runner and belong to the paid plan. A free workspace is refused before any dispatch. A build that names Safari with other browsers drops the Safari leg and builds the rest; Safari alone is refused | Raise the plan at extension.dev/pricing. Chrome, Edge and Firefox are never touched by this |
macos_notarization_not_included | A macOS launcher must be signed and notarized before macOS opens it. That signing belongs to the paid plan, and a free workspace is refused before anything is signed or submitted to Apple. The promotion itself succeeds | Raise the plan and promote again. Signing with your own Apple Developer account stays free |
safari_submissions_exhausted | The 15 App Store submissions a paid workspace carries in a month are spent. A submission also spends one from the monthly build allowance | Wait for the month to roll, or submit to the other three stores |
testing_workspace | The workspace is marked as a testing workspace and is read-only: builds, store submissions, releases, shares and AI generation are refused. Only an operator sets or clears the marker | None on the client. No retry or reset clears it |
Quota door codes
An endpoint with a person waiting answers at the door. The code comes in two dialects.
| Endpoint | Spent allowance | Testing workspace | Unreadable counter |
|---|---|---|---|
POST /builds/dispatch and the console Rebuild button | 429 with the reason as the code, such as monthly_cap | 403 testing_workspace | 503 counter_unavailable |
| Promote, rollback, cut and store submission | 429 BUILD_QUOTA_EXHAUSTED | 403 TESTING_WORKSPACE_READ_ONLY | 503 BUILD_COUNTER_UNAVAILABLE |
| Safari build or App Store submission | 403 macos_runner_not_included or 403 safari_submissions_exhausted | 503 SAFARI_ALLOWANCE_UNAVAILABLE |
The push webhook answers nobody, so push_builds_exhausted, fork_pull_request, burst_cap and unattributed are recorded against the workspace and read back in the console under Usage and limits.
macos_notarization_not_included is decided while a release is being promoted, after the promotion succeeded. It never sets the response status. It is reported inside the promote response under notarization.refused, with the code MACOS_NOTARIZATION_NOT_INCLUDED, the browsers it applies to and an upgrade link. Every extension package is published, and the Linux and Windows executables are built and downloadable.
Access gates
| Code | Status | Meaning |
|---|---|---|
UNAUTHENTICATED | 401 | No session and no token |
BAD_TOKEN | 401 | The token does not verify, or carries no project |
INVALID_PROJECT_ID | 400 | The {id} in the path is not a project id |
TOKEN_PROJECT_MISMATCH | 403 | The token names a different project |
OWNER_REQUIRED | 403 | The action is owner only, and the caller is not one |
A cut, a rollback and GET /api/projects/{id}/releases/promotion-runs are owner only. A request that carries a token is judged on the token and never falls back to the session. See the API reference.
When it fails
- A code is not on this page. The refusal came from the build itself, not from a gate. Open the build's logs with
GET /api/projects/{id}/builds/{buildId}/logs. - A
429with no reason you can place. Read thecode. The dispatch endpoint answers the reason itself, while promote, rollback, cut and submission answerBUILD_QUOTA_EXHAUSTED. Both mean the same spent month. - A promote answered
200and a browser is missing. ReadfailedBrowsersin the body. Only a promote where every dispatch failed answersRELEASE_DISPATCH_FAILED. - A promote answered
200and no macOS launcher shipped. Readnotarization.refusedin the body. The plan carries no signing, and the rest of the release went out.