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,.dmgor.zip. Keep it the same across releases (MyApp.pkg, notMyApp-1.2.0.pkg).version: a semantic version such as1.2.0, normally the app'sCFBundleShortVersionString. It must be higher than the previous release of the same file.build(recommended): the app's build number, such asCFBundleVersion(46or1.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.
latestmeans 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).
Download links
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.pkgManage
GET $API/spaces/my-app/artifactslists installers withdownloadsandbytes_served.GET $API/artifacts/ARTIFACT_IDshows one, including upload instructions while it is pending.DELETE $API/artifacts/ARTIFACT_IDremoves a version. Its URL then returns410 Gone,latestfalls back to the previous version, and the version cannot be reused.