heremyapp/docs llms.txt
View as Markdown

Installers

Publish app installers (.pkg, .dmg, .zip) with permanent download links. Each upload is one immutable version.

Before uploading (macOS)

heremyapp does not sign, notarize or vouch for installers. Sign the app and the .pkg with Developer ID, notarize and staple before uploading, then check:

spctl --assess --type install -vv MyApp.pkg    # expect "accepted" and "source=Notarized Developer ID"

Agents: if this check fails, stop and ask the user before uploading. Unsigned installers are blocked by Gatekeeper on users' Macs.

Notarizing needs credentials stored in the keychain (xcrun notarytool store-credentials PROFILE_NAME ..., then xcrun notarytool submit MyApp.pkg --keychain-profile PROFILE_NAME --wait). If no profile exists, ask the user to run store-credentials themselves in a terminal. It uses their Apple ID app-specific password or an App Store Connect API key; never ask them to paste those into the conversation.

Names and versions

  • filename: the bare file name, without folders. 1-100 characters of letters, digits, ., _ or -, ending in .pkg, .dmg or .zip. Keep it the same across releases (MyApp.pkg, not MyApp-1.2.0.pkg).
  • version: a semantic version such as 1.2.0, normally the app's CFBundleShortVersionString. It must be higher than the previous release of the same file.
  • build (recommended): the app's build number, such as CFBundleVersion (46 or 1.2.46). It must also increase.
  • notes (optional): a one-line summary of what changed. Show it on the landing page as release notes.
  • See Releases and versioning for how to choose the next version and build.
  • latest means the most recently published version, not the highest version number.

Because the filename is stable, the latest link is predictable before you upload: https://heremyapp.com/dl/ACCOUNT/SPACE/latest/FILENAME. Publishing the installer before the site means the link works as soon as the site is live.

Always use this absolute URL in the site. The site runs on its own origin, so a relative link such as /dl/... would point at the site itself and return 404.

1. Measure the file

FILE=./build/MyApp.pkg
NAME=$(basename "$FILE")
SIZE=$(stat -f%z "$FILE" 2>/dev/null || stat -c%s "$FILE")
SHA=$(shasum -a 256 "$FILE" | cut -d' ' -f1)

2. Register it

curl -sS --fail-with-body -X POST "$API/spaces/my-app/artifacts" -H "$AUTH" -H "content-type: application/json" \
  -d "{\"filename\":\"$NAME\",\"version\":\"1.2.0\",\"build\":\"46\",\"notes\":\"Faster sync\",\"size\":$SIZE,\"sha256\":\"$SHA\"}"

Success is 201 with the artifact in "status":"pending":

{"id":"art_3kq9...","space":"peter/my-app","filename":"MyApp.pkg","version":"1.2.0","size":18234567,
 "sha256":"4583...75fa","status":"pending",
 "download_url":"https://heremyapp.com/dl/peter/my-app/v/1.2.0/MyApp.pkg",
 "latest_url":"https://heremyapp.com/dl/peter/my-app/latest/MyApp.pkg",
 "upload":{"mode":"single","method":"PUT","url":"https://api.heremyapp.com/v1/artifacts/art_3kq9.../content",
  "command":"curl -sS --fail-with-body -T MyApp.pkg -H \"Authorization: Bearer $HEREMYAPP_TOKEN\" https://api.heremyapp.com/v1/artifacts/art_3kq9.../content"}}

For a larger file, upload looks like this instead:

{"mode":"multipart","part_size":52428800,"part_count":3,
 "part_url":"https://api.heremyapp.com/v1/artifacts/art_3kq9.../parts/{part_number}","complete_url":"https://api.heremyapp.com/v1/artifacts/art_3kq9.../complete",
 "command":"split -b 52428800 -a 3 MyApp.pkg /tmp/art_3kq9....part. && ..."}

upload.mode tells you which upload to do. upload.command is a ready-made shell command that contains no secrets. It refers to the file by its bare name, so run it from the folder that contains the file (cd "$(dirname "$FILE")"), with HEREMYAPP_TOKEN set. Or follow the steps below with your own path.

Registering the same filename and version again while it is still pending replaces the pending one.

3a. Upload in one request ("mode":"single", up to 95 MiB)

curl -sS --fail-with-body -T "$FILE" -H "$AUTH" "$API/artifacts/ARTIFACT_ID/content"

The response is the artifact with "status":"active".

3b. Upload in parts ("mode":"multipart", up to 500 MiB)

upload.part_size (in bytes; use it as PART_SIZE below) and upload.part_count describe the parts. Parts are numbered from 1, and every part except the last must be exactly part_size bytes. A failed part can simply be sent again.

split -b PART_SIZE -a 3 "$FILE" /tmp/heremyapp-part.
n=1; ok=1
for f in /tmp/heremyapp-part.*; do
  curl -sS --fail-with-body -T "$f" -H "$AUTH" "$API/artifacts/ARTIFACT_ID/parts/$n" || { ok=0; break; }
  n=$((n+1))
done
[ "$ok" = 1 ] && curl -sS --fail-with-body -X POST -H "$AUTH" "$API/artifacts/ARTIFACT_ID/complete"
rm -f /tmp/heremyapp-part.*

complete returns the artifact with "status":"active", or missing_parts listing parts still to send.

Verification

The server checks the exact size, the SHA-256 and the file signature (pkg = xar archive, dmg = UDIF image, zip). If a check fails (checksum_mismatch, invalid_file_content), the artifact is discarded: fix the file or the values and register again. Published versions are immutable, and each release must have a higher version and build than the last one (409 version_not_increasing or build_not_increasing, with suggested values in details).

  • latest_url: https://heremyapp.com/dl/ACCOUNT/SPACE/latest/FILENAME. Use this for the download button.
  • download_url: https://heremyapp.com/dl/ACCOUNT/SPACE/v/VERSION/FILENAME. This exact version, forever.

Downloads use Content-Disposition: attachment, support resumable Range requests and HEAD, and send the SHA-256 in ETag and X-Checksum-SHA256.

# quick check without downloading: which version latest points to, and its hash
curl -sIL https://heremyapp.com/dl/ACCOUNT/my-app/latest/MyApp.pkg | grep -iE "^location|^x-checksum-sha256"
# full check: download and hash
curl -fsSL -o /tmp/check.pkg https://heremyapp.com/dl/ACCOUNT/my-app/latest/MyApp.pkg && shasum -a 256 /tmp/check.pkg

Manage

  • GET $API/spaces/my-app/artifacts lists installers with downloads and bytes_served.
  • GET $API/artifacts/ARTIFACT_ID shows one, including upload instructions while it is pending.
  • DELETE $API/artifacts/ARTIFACT_ID removes a version. Its URL then returns 410 Gone, latest falls back to the previous version, and the version cannot be reused.