Skip to content
Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Latest commit

 

History

24 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BetaDrop Upload Action

Listed on the GitHub Marketplace Runners: ubuntu · macos · windows License: MIT

Turn a green CI build into an install link a tester can tap. This action uploads the .ipa or .apk your job just built to BetaDrop and hands back an over-the-air install link — no TestFlight review wait, no Play track, no tester accounts. Testers open the link on their phone and install from the browser.

- uses: betadrop-app/upload-action@v1
  with:
    file: app/build/outputs/apk/release/*.apk
    token: ${{ secrets.BETADROP_TOKEN }}

That is the whole integration. Create the token under Settings → Developer → API tokens at betadrop.app, store it as a repository secret, and the next push produces a link.

Where the link ends up

Three places, so nobody has to go looking for it:

Step output steps.<id>.outputs.install-url, for the rest of your workflow.
Run summary On the workflow run's own page — the whole team sees it, not just whoever triggered the build. Nothing to add.
Pull request With comment-on-pr: true, a comment that edits itself on every later push.

What lands on the run page

Every green build ends with this on the run summary. It is rendered below by the same Markdown the action writes, so this is the panel itself rather than a picture of one (the link is an example):

BetaDrop

Build published to BetaDrop

Install on a device · https://betadrop.app/install/?i=EXAMPLE

Open it on a phone to install over the air — no TestFlight, no tester accounts.

With comment-on-pr: true, the same link also arrives on the pull request, and every later push edits that comment instead of adding another:

Beta build ready to install

Install on a device

https://betadrop.app/install/?i=EXAMPLE

Open it on a phone to install over the air. Built from 0123456 · published by BetaDrop

When this is the right tool

Instead of What changes
TestFlight external testing No Beta App Review between a green build and a tester's phone, and no TestFlight app or invite to accept. Still ad-hoc signing rules — see the UDID note under Notes.
Play internal testing No Play Console upload step, no track, no tester list to keep in sync — you hand out a URL.
Diawi / similar OTA services Same idea, wired into CI with one step, plus standing links, PR comments and a step output.
Firebase App Distribution No Firebase project, no tester SDK, no invite acceptance — the link works in a plain mobile browser.

Both platforms, one step, one token.

Usage

- name: Publish to BetaDrop
  id: betadrop
  uses: betadrop-app/upload-action@v1
  with:
    file: app/build/outputs/apk/release/*.apk
    token: ${{ secrets.BETADROP_TOKEN }}

- name: Use the link
  run: echo "Install at ${{ steps.betadrop.outputs.install-url }}"

iOS example

jobs:
  beta:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v7
      - name: Build the .ipa
        run: fastlane gym --export_method release-testing
      - name: Publish to BetaDrop
        uses: betadrop-app/upload-action@v1
        with:
          file: "*.ipa"
          token: ${{ secrets.BETADROP_TOKEN }}

fastlane is preinstalled on the macos images, so fastlane gym runs without a setup step — use bundle exec fastlane gym only if your repository has a Gemfile pinning it.

release-testing is Apple's current name for ad-hoc distribution. gym copies this value straight into the ExportOptions plist's method key, where Apple deprecated ad-hoc in Xcode 15.4 — and Xcode 26, which is what macos-latest resolves to on GitHub-hosted runners, rejects it outright. If your fastlane is old enough to reject the new name, pass it through export_options: { method: "release-testing" } in a Fastfile instead. Either way the export runs before this action does, so getting it wrong fails the build a step early.

Android example

jobs:
  beta:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - name: Set up JDK
        uses: actions/setup-java@v6
        with: { java-version: "17", distribution: "temurin" }
      - name: Build release APK
        run: ./gradlew assembleRelease
      - name: Publish to BetaDrop
        uses: betadrop-app/upload-action@v1
        with:
          file: app/build/outputs/apk/release/*.apk
          token: ${{ secrets.BETADROP_TOKEN }}
          notes: ${{ github.event.head_commit.message }}

Post the link on the pull request

Set comment-on-pr and the reviewer gets the link without opening Actions:

jobs:
  beta:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write   # required for the comment
    steps:
      - uses: actions/checkout@v7
      - run: ./gradlew assembleRelease
      - uses: betadrop-app/upload-action@v1
        with:
          file: app/build/outputs/apk/release/*.apk
          token: ${{ secrets.BETADROP_TOKEN }}
          comment-on-pr: true

Every later push to the branch edits that same comment rather than adding another, so a twenty-commit PR ends with one comment holding the current build — not twenty holding stale ones.

If the comment cannot be posted (usually a missing pull-requests: write), the step says so as a warning and the run stays green: the build published, and the link is still an output and still on the run summary. On an event with no pull request — a push to main, a manual dispatch — the comment is skipped silently.

Prefer to build the comment yourself? The link is a normal step output:

- name: Publish to BetaDrop
  id: betadrop
  uses: betadrop-app/upload-action@v1
  with:
    file: "*.apk"
    token: ${{ secrets.BETADROP_TOKEN }}

- run: echo "Install at ${{ steps.betadrop.outputs.install-url }}"

Inputs

Input Required Default Description
file yes Path to the .ipa/.apk. Globs allowed; must match exactly one file.
token yes A BetaDrop API token (bd_live_…). Create one under Settings → Developer → API tokens at betadrop.app, and store it as a repository secret.
name no Override the build name shown on the install page.
notes no Release notes for this build — e.g. the commit message.
standing-link no A standing link slug to point at this build once it is live — e.g. acmebeta. That one URL, and any "Download from BetaDrop" button embedded with it, then serves this build with nothing to click. Pro and Studio plans; claim the name first. A name that cannot be used fails the step before anything is uploaded. Needs @betadrop/cli 0.3.0+ — the step verifies this before uploading.
channel no Deprecated alias for standing-link — the same feature under its previous name. Permanently accepted, so a workflow already pinning it keeps working. If both are set, standing-link wins.
expires-in-days no Stop the install link working after this many days. Clamped to your plan's maximum; the log says when. Needs @betadrop/cli 0.4.0+.
max-downloads no Stop it after this many downloads. Needs @betadrop/cli 0.4.0+.
max-devices no Stop it after this many different devices. Set more than one limit and the link stops at whichever comes first. Needs @betadrop/cli 0.4.0+.
inspect no false Check the build will install from a link before uploading it, and fail the step with nothing uploaded if it will not. Needs @betadrop/cli 0.4.0+.
comment-on-pr no false Post the install link as a comment on the pull request, editing that same comment on later pushes. Needs pull-requests: write. Skipped on events with no PR.
github-token no ${{ github.token }} Token used for that comment. Override only if it should come from another account.
cli-version no latest Version of @betadrop/cli to use. Pin for reproducible pipelines.

Outputs

Output Description
install-url The OTA install link for the uploaded build.
standing-link-url The standing link's URL when standing-link (or channel) was set; empty otherwise.
channel-url Deprecated alias for standing-link-url. Same value, kept so existing workflows keep working.

One link that never changes

A new build means a new install link, which means re-sending it. A standing link is a fixed URL you re-point at each new build — hand it out once and stop re-sending:

- name: Publish to BetaDrop
  id: betadrop
  uses: betadrop-app/upload-action@v1
  with:
    file: app/build/outputs/apk/release/*.apk
    token: ${{ secrets.BETADROP_TOKEN }}
    standing-link: acmebeta

- run: echo "Testers keep using ${{ steps.betadrop.outputs.standing-link-url }}"

If the build uploads but the standing link could not be moved, the step fails — a pipeline that asked for one must not go green when it did not move.

standing-link needs @betadrop/cli 0.3.0 or newer. The step asks the installed CLI which spelling it understands and uses that, so a pinned 0.3.0 keeps working unchanged; if the version is older than either, the step names it rather than surfacing commander's noise. Every other input works on any published version.

Refuse a build that will not install

The expensive signing mistakes surface late: a build exported with an App Store profile, a profile that expired last week, or an APK Android Studio marked testOnly all upload fine and then fail on a tester's phone. inspect: true reads the build first and fails the step — with nothing uploaded — when it would not install from a link:

- uses: betadrop-app/upload-action@v1
  with:
    file: build/*.ipa
    token: ${{ secrets.BETADROP_TOKEN }}
    inspect: true
    expires-in-days: 14

The run log lists each check. It covers the provisioning profile's type and expiry, a profile issued for a different bundle ID, unsigned and testOnly APKs, and v1-only signatures on apps targeting Android 11 or later.

Tell Slack when the build lands

Webhooks live on your account rather than in the workflow, so every upload path — this action, the CLI, the dashboard — posts to the same place. Add one once:

npx @betadrop/cli webhooks add https://hooks.slack.com/services/T000/B000/XXXX

Discord webhook URLs work the same way, and any other URL receives signed JSON. See https://betadrop.app/docs/webhooks/.

A download button that follows the standing link

Wrap the standing link in the "Download from BetaDrop" badge and paste it in your README — one Markdown image link, no script. With standing-link: set on the step above, every merge updates what the button installs and the README never changes.

[![Download MyApp from BetaDrop](https://betadrop.app/badge/download-dark.svg)](https://betadrop.app/install/acmebeta?ref=badge)

Copy the exact snippet (dark or light, HTML or Markdown) from the embed panel under any build's install link. Details and guidelines: https://betadrop.app/download-button/.

Pin the CLI for a reproducible pipeline

cli-version defaults to latest, which means a green pipeline can change behaviour on a day you did not touch it. Pin it once you are past setup:

- uses: betadrop-app/upload-action@v1
  with:
    file: "*.ipa"
    token: ${{ secrets.BETADROP_TOKEN }}
    cli-version: "0.4.0"

Troubleshooting

the installed @betadrop/cli … supports neither --standing-link nor --channel — the version that resolved is older than 0.3.0, which is where the flag landed. Set cli-version to 0.3.0 or newer.

The PR comment never appears — check the run log for the warning. The usual cause is the job lacking permissions: pull-requests: write; the other is an event with no pull request, which is reported as a notice. Forked-PR runs get a read-only token by default and cannot comment at all.

No file matches '…' — the glob ran before the build wrote anything, or wrote somewhere else. Add a run: ls -R step before this one to see what the build actually produced. A pattern that resolves only to directories counts as no match.

'…' matches N files — deliberate. Publishing an arbitrary one of several builds is worse than failing, so narrow the pattern until it names one.

Publish did not end with an install URL — the CLI printed something after the link, which breaks the contract this action reads. Report it; the last line of the run log is the useful detail.

It downloads on the phone but will not open (iOS) — see the note about UDIDs below.

Notes

  • Works on ubuntu, macos, and windows runners (Node is preinstalled on all GitHub-hosted images) — this repo's own selftest runs the shipped script on all three.
  • A glob matching more than one file fails the step rather than uploading an arbitrary build — narrow the pattern.
  • Pass token as a repository secret. A token pasted literally into a workflow is registered as a mask by the step, but it is still committed to your repository in clear.
  • Build links follow your account's retention settings; see pricing.
  • An ad-hoc iOS build uploaded from CI still only installs on registered devices. If a tester reports "it downloads but won't open", their UDID was not in the provisioning profile when the IPA was signed. Send them the UDID checker — it reads the UDID off the iPhone itself, so they don't need a Mac or a cable — then add the device in the Apple Developer portal and re-sign.
  • This action is a thin wrapper over the same @betadrop/cli you'd use locally, so CI behaves exactly like your machine.

Also from BetaDrop

The same publish step, on the other two surfaces — one account, one set of API tokens:

@betadrop/cli
source
betadrop publish app.ipa from your terminal. Prints the install link and a scannable QR code.
@betadrop/mcp
source
MCP server — publish builds, list history and manage tokens by asking Claude, Cursor or Copilot, without leaving the editor.

Contributing

Issues and pull requests are welcome — see SECURITY.md for reporting a vulnerability privately. If this saved you a TestFlight round-trip, a ⭐ helps other mobile teams find it.

License

MIT

About

Upload an iOS .ipa or Android .apk to BetaDrop from CI and get an instant over-the-air install link.

Topics

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors