MoccoDocs

Publish with curl

The Action is a convenience: every step is plain HTTP against Mocco's public API, so any CI system (or a script on your machine) can publish. You need a secret API key with the desktop:write scope, your app id (Setup), curl, jq and shasum.

The five calls

All calls take Authorization: Bearer <key>. Errors answer application/problem+json with a detail that says what to fix.

  1. Declare the version and its files: POST /v1/desktop/apps/<app id>/releases with the version, the notes, and per file its platform, kind (installer or updater), file name, content type, size and SHA-256 (plus the .sig content for an updater). It answers 201 (or 200 when the version was declared before) with a releaseId and, per file, an upload URL, or null for a file already verified.

  2. Upload each file with PUT to its URL, with exactly the headers it came with, within 15 minutes.

  3. Finalize: POST /v1/desktop/releases/<release id>/finalize. It answers 202. If an upload is missing or incomplete, it answers 400 naming the file: declare again with the same body (fresh URLs), upload, finalize.

  4. Wait: GET /v1/desktop/releases/<release id> until status is draft (verified) or failed (failureReason says which file and why).

  5. Publish: POST /v1/desktop/releases/<release id>/publish with {"channel":"stable"}. It answers 201 (or 200 when the channel already serves it).

A published or withdrawn version can't be declared again (409 version_exists): bump the version.

The script

API=https://www.mocco.club/api/ext/v1
APP=$MOCCO_DESKTOP_APP_ID
AUTH="Authorization: Bearer $MOCCO_API_KEY"
V=0.2.0
DMG=release/App_${V}_aarch64.dmg
TGZ=release/App_${V}_aarch64.app.tar.gz

artifact() { # platform kind file content-type [signature-file]
  jq -n --arg platform "$1" --arg kind "$2" --arg filename "$(basename "$3")" --arg contentType "$4" \
    --argjson size "$(wc -c < "$3" | tr -d ' ')" --arg sha256 "$(shasum -a 256 "$3" | cut -d' ' -f1)" \
    --arg signature "$( [ -n "${5:-}" ] && cat "$5" )" \
    '{platform:$platform, kind:$kind, filename:$filename, contentType:$contentType, size:$size, sha256:$sha256}
     + (if $signature == "" then {} else {signature:$signature} end)'
}
BODY=$(jq -n --arg version "$V" --rawfile notes RELEASE_NOTES.md \
  --argjson dmg "$(artifact darwin-aarch64 installer "$DMG" application/x-apple-diskimage)" \
  --argjson tgz "$(artifact darwin-aarch64 updater "$TGZ" application/gzip "$TGZ.sig")" \
  '{version:$version, notes:$notes, artifacts:[$dmg, $tgz]}')

# 1. Declare.
DECLARED=$(curl -fsS -X POST "$API/desktop/apps/$APP/releases" -H "$AUTH" -H 'content-type: application/json' -d "$BODY")
RELEASE=$(jq -r .releaseId <<<"$DECLARED")

# 2. Upload each file that has a URL (null: already verified).
put() { # platform kind file
  TARGET=$(jq -c --arg p "$1" --arg k "$2" '.uploads[] | select(.platform == $p and .kind == $k) | .upload' <<<"$DECLARED")
  [ "$TARGET" = null ] && return 0
  HEADERS=(); while IFS= read -r h; do HEADERS+=(-H "$h"); done \
    < <(jq -r '.headers | to_entries[] | "\(.key): \(.value)"' <<<"$TARGET")
  curl -fsS -X PUT "${HEADERS[@]}" --upload-file "$3" "$(jq -r .url <<<"$TARGET")"
}
put darwin-aarch64 installer "$DMG"
put darwin-aarch64 updater "$TGZ"

# 3. Finalize, and 4. wait for draft or failed.
curl -fsS -X POST "$API/desktop/releases/$RELEASE/finalize" -H "$AUTH"
while :; do
  STATUS=$(curl -fsS "$API/desktop/releases/$RELEASE" -H "$AUTH")
  case $(jq -r .status <<<"$STATUS") in
    draft) break ;;
    failed) jq -r .failureReason <<<"$STATUS" >&2; exit 1 ;;
    *) sleep 5 ;;
  esac
done

# 5. Publish to stable.
curl -fsS -X POST "$API/desktop/releases/$RELEASE/publish" -H "$AUTH" -H 'content-type: application/json' \
  -d '{"channel":"stable"}'

The script runs on macOS and Linux runners. curl -f fails the step on an error; to see the problem's detail, drop -f and print the body. Running it again for the same version resumes: files already verified aren't uploaded again.

The full API, including notes and yank, is in the desktop releases reference.