The upload
What publish sends, and what the receiving service is responsible for.
Keep the checkout clean
Publish requires committed HEAD and a clean checkout before the build and again before upload. Commit source changes before the job runs. Keep build products ignored by Git. Mokly builds in a private directory outside the checkout and prints its location before rendering. Ignored files and operation-owned reservations do not count as uncommitted changes.
Use npm ci to install the committed dependency lockfile. A job that changes
tracked sources or creates non-ignored files must resolve those changes before
it publishes.
Three steps
Publish talks to the endpoint you gave it in three steps and presents the token as a bearer credential on every request. Every request goes to the endpoint’s own origin, the endpoint’s path is never extended, and none follows a redirect.
The plan
Publish posts one small gzip-compressed tar archive to the exact endpoint,
which is the service’s plan route. It holds mokly-upload.json, the ownership
marker that lists every output file with its SHA-256 digest and byte size,
and the catalogue index, mokly-viewer/index.json, which also carries the
Changes of the build. Nothing else is in it.
Authorization: Bearer TOKEN
Content-Type: application/gzip
Accept: application/json
Content-Length: <bytes>The service answers 200 with JSON: an upload id and its expiry time, the
sorted list of digests it does not yet hold, a blob URL containing the literal
placeholder {sha256}, and a completion URL. Publish takes both URLs only from
this answer and never builds them from the endpoint. Both must be on the
endpoint’s own scheme, host and port and use HTTP or HTTPS; blob: and other
schemes are refused before a request. Publish never sends the token elsewhere.
The blobs
For each missing digest, publish sends the raw bytes, or a gzip body when the
service lists gzip in the Plan and the gzip body is smaller than the file. It
fills the digest into the blob URL and respects the CLI’s
--upload-concurrency limit.
Authorization: Bearer TOKEN
Content-Type: application/octet-stream
Content-Length: <body length>
Content-Encoding: gzipContent-Encoding: gzip appears only on a gzip body. Any 2xx answer means
the file is stored. 400 means invalid gzip or decoded bytes that do not match
the declared digest or size, and 404 means the digest was not part of the
plan.
The completion
Publish posts to the completion URL with an empty body.
Authorization: Bearer TOKEN
Accept: application/json
Content-Length: 0201 means this upload created the publication. Repeating Complete for that
upload returns its first status and body and never publishes again. 200 means
a different upload already completed this revision and config path, so the
service kept and returned the first publication; publish then prints
Mokly catalogue already published for this commit. instead of counts. Any
other 2xx fails. When the JSON answer carries an accepted absolute
viewerUrl, publish prints it after success. 409 means the service still
lacks files and 410 means expiry: publish plans once more, uploads what comes
back, and completes again; a second 409 or 410 fails.
What the manifest says
mokly-upload.json names the Mokly version, the repository, the branch, the
head revision, the base ref and pinned base revision, the pull request number
when the job is running on one, the config path and the time the build
finished. Without comparisons, baseRef and baseSha are both null.
Receivers must reject missing or extra manifest fields.
The source inventory stays in memory. The output contains the public catalogue index and entry details. It contains no source manifest or consumer configuration.
What the receiver must do
A service first authenticates the bearer credential. Only then does it
decompress the plan archive, enforcing size and file-count limits before
reporting version or structural failures. It validates the manifest’s fields
and version, the marker’s schema, digests, sizes and paths, stores the three
archived artifacts once their bytes match their digests, and compares the
remaining digests with every verified Blob stored for that project, including
content received for an unfinished or expired upload. Each stored Blob must
match its declared digest and size. A gzip body must decode within its declared
file size, and the service checks its decoded digest and size before storage.
The service lists gzip only when it decodes gzip Blobs for the whole upload.
An integer declared size above 64 MiB or a path above 1,024 UTF-8 bytes
receives 413; malformed values and path grammar receive 400 or 422.
The service commits only when every listed digest is present and answers 409
until then. It keeps the first publication completed for a revision and config
path. Before exposure, it checks that the credential is allowed to publish for
the repository named by the manifest.
Receivers accept only regular files. They reject symlinks, hard links, devices, FIFOs, sparse files and other special entries in the plan archive, and extract into an empty private directory so no path can escape it.
Retries and timeouts
Each request times out after 120 seconds. A request that fails in transit, or
is answered 408, 429, 500, 502, 503 or 504, is tried at most five
times. Random waits before attempts two through five are at most 1, 2, 4 and 8
seconds, or the delay a valid Retry-After of up to sixty seconds asks for.
Nothing is retried at or past Plan expiry; publish instead plans once more, as
after a 410, and everything already stored stays held.
Counts and cancellation
The uploaded count includes marker entries whose digest matches a file sent in
the Plan archive and every entry sharing a digest whose Blob PUT was attempted
in this command. Every other marker entry is unchanged. Progress applies the same rule
per round, begins with Plan files, and advances all entries sharing a completed
digest. <size> counts decoded file bytes, including the raw bytes of Plan
files, even when a Blob request uses gzip. It says
Uploading 0 of 1 file · <size> for one file and uses files
otherwise; an empty missing set shows no progress label.
Cancellation prints
[mokly/upload-failed] Publication was cancelled. Run mokly publish again when you are ready.
If Mokly could not put your previous output back, it prints the recovery error
with the folder to recover instead, even when you cancelled.
An exhausted transport failure instead tells the reader to check the endpoint
and connection, then retry.
When it is refused
| Result | Category |
|---|---|
| The token or the repository was refused | upload-unauthorized |
| The Plan archive, manifest or marker was malformed, or Blob bytes did not match | upload-invalid-bundle |
| An upload limit or declared size/path byte limit was exceeded | upload-too-large |
| The manifest or marker declares an unknown version | upload-unsupported-version |
Anything else, including a timeout or a second 409 or 410 |
upload-failed |
A failed publish leaves the complete local build where it was written, so you can look at exactly what would have been sent, and running it again resumes from what the service already stored. The upload reference holds the full contract.