Upload every app build from CI and get an install link
· 6 min read
If someone on your team still downloads a build from CI and drags it into a browser to share it, that step can go. Every pipeline that produces an .apk or .ipa can upload it at the end of the job and print an install link. This guide shows the same upload in GitHub Actions, GitLab CI, Bitrise, Jenkins, Fastlane, Codemagic and Xcode Cloud, with release notes taken from the commit message and the link passed on to the next step.
The one command every pipeline runs
All the examples below call the AppForShare CLI through npx, so nothing has to be installed ahead of time apart from Node 18 or newer:
npx --yes appforshare@1 upload path/to/app.ipa --notes "What changed"
What it does, based on the CLI itself:
- Reads the API token from the
APPFORSHARE_TOKENenvironment variable (or--token). - Uploads in 32 MB chunks and resumes from where it stopped if the connection drops, retrying network and server errors.
- Waits for processing, then prints the install link. With
--jsonit prints{ url, slug, name, version, expiresAt }instead. - In GitHub Actions it also writes
urlandslugto the step outputs. - Exits with code 1 and a message starting with
appforshare:if anything fails, so the job fails visibly.
Other options: --notes-file reads release notes from a file, and --no-wait returns as soon as the upload is queued, without the link. Pinning appforshare@1 keeps your pipeline on the 1.x line.
Get a token
CI upload tokens are part of the Pro and Team plans. Create one under Dashboard, Settings, and store it as a secret in your CI under the name APPFORSHARE_TOKEN. Never put it in the repository. Release notes are also a Pro and Team feature; on a plan without them the upload stops with "Release notes are part of Pro and Team."
What to upload
Upload an APK, not an .aab, and for iPhone an IPA exported Ad Hoc, Development or Enterprise. App Store exports cannot be installed from a link, and AppForShare does not re-sign builds. If you are not sure what your pipeline exports, open the IPA in the IPA inspector once. iOS code signing errors on test builds covers the export side.
GitHub Actions
- name: Share build
id: afs
run: npx --yes appforshare@1 upload app/build/outputs/apk/release/app-release.apk --notes "$NOTES"
env:
APPFORSHARE_TOKEN: ${{ secrets.APPFORSHARE_TOKEN }}
NOTES: ${{ github.event.head_commit.message }}
- run: echo "Install link: ${{ steps.afs.outputs.url }}"
Pass the commit message through an environment variable as above rather than writing ${{ github.event.head_commit.message }} straight into the run line. A commit message is text anyone with push access controls, and putting it directly in a shell command lets quotes or backticks in it run as code. Note that head_commit is only set on push events; on other triggers use git log -1 --pretty=%B.
GitLab CI
share:
stage: deploy
image: node:22
needs: [build_android]
script:
- npx --yes appforshare@1 upload app-release.apk --notes "$CI_COMMIT_MESSAGE" --json > appforshare.json
- node -p "require('./appforshare.json').url"
artifacts:
paths: [appforshare.json]
Add APPFORSHARE_TOKEN as a masked CI/CD variable. Because iOS builds need a macOS runner and the upload does not, many teams build on the Mac runner and upload in a small Node job that picks up the IPA as an artifact.
Bitrise
Add a Script step after the step that exports the build. Bitrise puts the exported file path in BITRISE_IPA_PATH or BITRISE_APK_PATH. Add the token under Secrets as APPFORSHARE_TOKEN.
- script@1:
title: Share build
inputs:
- content: |-
#!/usr/bin/env bash
set -euo pipefail
npx --yes appforshare@1 upload "$BITRISE_IPA_PATH" \
--notes "$(git log -1 --pretty=%B)" --json > afs.json
envman add --key AFS_INSTALL_URL --value "$(node -p "require('./afs.json').url")"
envman add makes the link available to later steps, for example a chat message step.
Jenkins
stage('Share build') {
steps {
withCredentials([string(credentialsId: 'appforshare-token', variable: 'APPFORSHARE_TOKEN')]) {
sh 'npx --yes appforshare@1 upload app/build/outputs/apk/release/app-release.apk --notes "$(git log -1 --pretty=%B)"'
}
}
}
Store the token as a "Secret text" credential. Use single quotes around the sh command so the shell, not Groovy, expands the variables, which also keeps the token out of the build log.
Fastlane
lane :beta_ios do
gym(export_method: "ad-hoc")
sh("npx", "--yes", "appforshare@1", "upload",
lane_context[SharedValues::IPA_OUTPUT_PATH],
"--notes", last_git_commit[:message])
end
lane :beta_android do
gradle(task: "assembleRelease")
sh("npx", "--yes", "appforshare@1", "upload",
lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH],
"--notes", last_git_commit[:message])
end
Passing the arguments separately to sh avoids quoting problems with commit messages. Recent Xcode versions print a deprecation warning for the ad-hoc export name in favour of release-testing; check which value your Fastlane version expects.
Codemagic
workflows:
android-test:
environment:
groups:
- appforshare
scripts:
- name: Build
script: flutter build apk
- name: Share build
script: |
npx --yes appforshare@1 upload build/app/outputs/flutter-apk/app-release.apk \
--notes "$(git log -1 --pretty=%B)"
Put APPFORSHARE_TOKEN in an environment variable group (here named appforshare) and mark it secret. For iOS, point the upload at the exported IPA, for example the file under build/ios/ipa/ in a Flutter project.
Xcode Cloud
Xcode Cloud has no upload step of its own for other services, but it runs a script named ci_post_xcodebuild.sh from a ci_scripts folder in your repository after each xcodebuild action. The script must be executable (chmod +x). Homebrew is available on the build machines, so Node can be installed when it is missing.
#!/bin/sh
set -e
# Runs after every xcodebuild action; only upload after an archive.
[ "$CI_XCODEBUILD_ACTION" = "archive" ] || exit 0
if [ -z "$CI_AD_HOC_SIGNED_APP_PATH" ]; then
echo "No Ad Hoc export in this build, skipping upload"
exit 0
fi
IPA=$(find "$CI_AD_HOC_SIGNED_APP_PATH" -name "*.ipa" -print -quit)
command -v npx >/dev/null || brew install node
NOTES=$(git -C "$CI_PRIMARY_REPOSITORY_PATH" log -1 --pretty=%B)
npx --yes appforshare@1 upload "$IPA" --notes "$NOTES"
Add APPFORSHARE_TOKEN as a secret environment variable in the workflow settings. The important variable is CI_AD_HOC_SIGNED_APP_PATH: it points to the Ad Hoc signed export of the archive when one exists. If it is empty, upload nothing, because the App Store signed export cannot be installed from a link.
Using the link after the upload
The point of --json is to hand the link to whatever comes next: a comment on the pull request, a message in a team channel, an issue tracker field. With Node already on the machine, reading it needs no extra tools:
npx --yes appforshare@1 upload app.apk --json > afs.json
URL=$(node -p "require('./afs.json').url")
EXPIRES=$(node -p "require('./afs.json').expiresAt")
With --json, progress lines are not printed, so the file contains only the JSON. If you want the link in Slack or Discord, you may not need a script at all: AppForShare webhooks can post every ready build there. Posting build links to Slack and Discord compares both ways.
Troubleshooting
| Message | Meaning |
|---|---|
No API token. Pass --token or set APPFORSHARE_TOKEN. | The secret is not exposed to this step. Check the variable name and that the job can read secrets (forked pull requests usually cannot). |
Release notes are part of Pro and Team. | The token's account is on a plan without release notes. Drop --notes or upgrade. |
Unknown option ... | A typo in a flag, or a commit message starting with a dash was passed without quotes. |
Processing is taking too long. Check your dashboard. | The CLI waits up to 20 minutes for processing. The build may still finish; check the dashboard. |
Size limits per file are 2 GB on Pro and 4 GB on Team. Other upload failures are described in the most common upload errors.
Set up your pipeline
CI upload tokens, release notes, webhooks and install analytics start on Pro at $12 a month, and links stay live while the plan is active. Team at $39 a month adds roles, private projects for invited testers and emails to testers on each new build. The examples above are also on the CI docs page. See the pricing page for every limit, or create an account and generate your first token.


