Skip to content
Continuous integrationThe upload

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.

http
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.

http
Authorization: Bearer TOKEN
Content-Type: application/octet-stream
Content-Length: <body length>
Content-Encoding: gzip

Content-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.

http
Authorization: Bearer TOKEN
Accept: application/json
Content-Length: 0

201 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.