Build and package a Makepad app for macOS, Windows, Linux, Android and iOS, and optionally upload the results to a GitHub Release.
Ready-to-copy workflows live in examples/.
- Quick start
- Packaging Details
- Action Reference
- Goals
- Inputs
- Environment variables
- Packaging tool versions
- Outputs
- How it works
- Behavior notes
- Verifying
.debdependencies - iOS (cargo-makepad) reference
- macOS signing and notarization convenience
- Updater signatures
- Placeholder replacement
- Release Modes
- iOS signing convenience
- Example: matrix release
- Example: upload to an existing release
- Example: Android only
- Development
- Contributing
- License
The action builds and packages, but it does not check out your code or install a Rust toolchain, so a working job needs at least:
jobs:
package:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
# Makepad's Linux system dependencies. Not needed on macOS or Windows.
- run: |
sudo apt-get update
sudo apt-get install -y libssl-dev pkg-config llvm clang libclang-dev \
binfmt-support libxcursor-dev libx11-dev libasound2-dev libpulse-dev \
libwayland-dev libxkbcommon-dev libegl1
- uses: project-robius/makepad-packaging-action@v1.7.0
with:
packager_formats: deb@v1 also works and floats to the newest v1.x release; pin an exact tag when you want
builds to stay reproducible.
cargo-packager and
robius-packaging-commands
are used under the hood to create the packages. Both are installed for you; see
Packaging tool versions.
cargo-makepad is used
to build the mobile applications for iOS and Android platforms.
It is installed from the exact makepad revision your Cargo.lock pins for
makepad-widgets, so the build tool always matches the makepad your app depends on,
including forks. See Packaging tool versions for the fallback
rules.
Two of these are enforced: targeting macOS or iOS from a non-macOS host fails immediately.
The rest are practical limits that surface later as a build error rather than an upfront
check, and none of the checks run at all unless you pass a --target triple.
- Linux packages on a Linux OS machine
- Windows installer executables on a Windows OS machine
- macOS disk images / app bundles on a macOS machine (enforced)
- iOS apps on a macOS machine (enforced)
- Android, on a machine with any OS! (building on Windows or macOS logs a warning)
- One-step packaging for Makepad desktop and mobile targets
- GitHub Release upload with optional tag/name/body templating
- Sensible defaults sourced from
Cargo.toml - Matrix-friendly usage (pass
argsto target specific triples)
These inputs are already defined in action.yaml. Build and packaging inputs are
snake_case; GitHub Release inputs are camelCase, matching the API they wrap.
args: extra build args (e.g.--target x86_64-unknown-linux-gnu). On desktop these are forwarded tocargo packager; on mobile only--targetis read, to pick iOS vs Android, and the rest is ignored (use the*_CARGO_EXTRA_ARGSenv vars forcargo makepadflags).--releaseis appended automatically for the release build.packager_formats: comma-separated formats forcargo packager(e.g.deb,dmg,nsis). Ignored if--formatsis already present inargsorpackager_args. Requestingdebon Linux also triggers apt-file setup.packager_args: extra args passed only tocargo packagerrobius_packaging_commands_version: pin the version ofrobius-packaging-commandsinstalled for desktop packaging (e.g.0.3.3). A pinned version is installed even if another one is already on PATH. When unset, an existing copy on PATH is reused as-is and only an absent tool is fetched, in which case the latest published version is installed.verify_deb: verify each built.debdeclares every runtime dependency it actually uses, before any artifact is uploaded (default:false). See Verifying.debdependencies.verify_deb_args: extra args passed toverify-deb(e.g.--host,--image ubuntu:22.04,--run-secs 20)verify_strict: whentrue(default), a failed verification fails the job and nothing is uploaded;falsereports warnings and uploads anywaytagName: GitHub Release tag, supports__VERSION__placeholder. If omitted and the workflow runs on a tag ref, that tag is used.releaseName: Release title, supports__VERSION__placeholderreleaseBody: Release body markdownreleaseId: existing GitHub Release ID (uploads assets to this release and skips release creation).tagName,releaseName,releaseBody,releaseDraft,prereleaseandgenerateReleaseNotesare all ignored when this is set.releaseCommitish: branch/commit SHA for creating tag/release (default: current commit SHA)uploadUpdaterJson: upload/updatelatest.jsonupdater metadata asset on the release (default:true)uploadUpdaterSignatures: upload.sigfiles (if present next to built assets) and include signatures inlatest.json(default:true)retryAttempts: additional retry attempts for release-asset/latest.json upload conflicts (default:0). Uploads always get at least 2 attempts, so this adds to that floor.owner: release target repository owner (default: current repo owner)repo: release target repository name (default: current repo name)githubBaseUrl: custom GitHub API base URL for GHE/self-hosted APIs (default: envGITHUB_API_URL, which GitHub Actions always sets)generateReleaseNotes: use GitHub generated release notes when creating a release (default:false)releaseAssetNamePattern: pattern naming for uploaded assets, supports[app] [name] [version] [platform] [arch] [mode] [ext] [filename] [basename]. Ignored, with a warning, whenasset_name_templateis also set.asset_name_template: template for asset names (__APP__,__VERSION__,__PLATFORM__,__ARCH__,__MODE__,__EXT__,__FILENAME__,__BASENAME__). Takes precedence overreleaseAssetNamePattern.asset_prefix: optional prefix prepended to generated asset namesreleaseDraft: create the release as a draft (default:false); ignored whenreleaseIdis setprerelease: mark as a prerelease (default:false); ignored whenreleaseIdis setgithub_token: token for release creation/upload (defaults to envGITHUB_TOKEN)project_path: Makepad project root, resolved relative to the working directory (default:.)projectPath: alias ofproject_path;project_pathwins if both are setapp_name: override app name (auto fromCargo.tomlif omitted)app_version: override version (auto fromCargo.tomlif omitted)identifier: override bundle identifier. Mobile only: it becomes the Android package name and seeds the iOS org/app derivation. Desktop packaging ignores it and uses the identifier in[package.metadata.packager]. Defaults toorg.makepad.<crate name>.include_release: include release build (default:true)include_debug: include debug build (default:false). Enabling both re-scans the same output directory, so release artifacts are collected a second time and labelleddebug.upload_to_testflight: upload iOS IPA to TestFlight (default:false). OR'd withMAKEPAD_IOS_UPLOAD_TESTFLIGHT, so either one enables it and setting this tofalsewill not switch off an upload the env var turned on.enable_macos_notarization: enable macOS APP_STORE_CONNECT -> APPLE_API credential mapping (default:false). Also OR'd withMAKEPAD_MACOS_ENABLE_NOTARIZATION.
Mobile and signing configuration is provided via env vars only. Booleans accept
true/1/yes/on (case-insensitive); anything else is false.
-
MAKEPAD_ANDROID_ABI: Android ABI to build (x86_64,aarch64,armv7,i686), defaultaarch64. This, not the--targettriple, is what selects the ABI, so--target x86_64-linux-androidstill produces anaarch64build unless you set this.allis rejected. -
MAKEPAD_ANDROID_FULL_NDK: install full Android NDK (true/false), defaultfalse -
MAKEPAD_ANDROID_VARIANT: Android build variant (default,quest), defaultdefault -
MAKEPAD_MOBILE_CARGO_EXTRA_ARGS: extra args appended to both iOS and Androidcargo makepadbuild commands -
MAKEPAD_ANDROID_CARGO_EXTRA_ARGS: extra args appended only to Androidcargo makepadbuild commands -
MAKEPAD_IOS_ORG: iOS org identifier (e.g.com.example). If unset, it is derived fromidentifier(everything before the last dot), falling back toorg.makepad, which will not match a real provisioning profile. -
MAKEPAD_IOS_APP: iOS app name. Falls back toapp_name, then the crate's binary name; the build only fails if none of the three resolve. -
MAKEPAD_IOS_PROFILE: provisioning profile UUID or path (optional, auto-derived when Apple envs are set) -
MAKEPAD_IOS_CERT: signing certificate fingerprint (optional, auto-derived when Apple envs are set) -
MAKEPAD_IOS_SIM: build for iOS simulator (true/false), defaultfalse -
MAKEPAD_IOS_CREATE_IPA: create IPA from .app bundle (true/false), defaultfalse. Ignored for simulator builds. -
MAKEPAD_IOS_UPLOAD_TESTFLIGHT: upload IPA to TestFlight (true/false), defaultfalse -
MAKEPAD_IOS_CARGO_EXTRA_ARGS: extra args appended only to iOScargo makepadbuild commands -
APP_STORE_CONNECT_API_KEYorAPP_STORE_CONNECT_API_KEY_CONTENT: App Store Connect API key content (.p8PEM text) -
APP_STORE_CONNECT_API_KEY_CONTENT_BASE64(orAPP_STORE_CONNECT_API_KEY_BASE64): base64-encoded.p8content (optional alternative to plain PEM text) -
APP_STORE_CONNECT_KEY_ID: App Store Connect key ID -
APP_STORE_CONNECT_ISSUER_ID: App Store Connect issuer ID -
APPLE_CERTIFICATE: base64-encoded Apple signing certificate (.p12) -
APPLE_CERTIFICATE_PASSWORD: password for the certificate -
APPLE_PROVISIONING_PROFILE: base64-encoded provisioning profile (.mobileprovision) -
APPLE_KEYCHAIN_PASSWORD: password for the temporary keychain -
APPLE_SIGNING_IDENTITY: signing identity common name used to locate the certificate (default:Apple Distribution). If nothing matches, the first certificate in the temporary keychain is used rather than failing, so a typo here signs with the wrong identity. -
APPLE_KEYCHAIN_PROFILE: optional notarization keychain profile for macOSnotarytool -
APPLE_ID/APPLE_PASSWORD/APPLE_TEAM_ID: optional Apple ID notarization credentials for macOS -
APPLE_API_KEY/APPLE_API_ISSUER/APPLE_API_KEY_PATH: optional App Store Connect notarization credentials for macOS -
MAKEPAD_MACOS_ENABLE_NOTARIZATION: optional env fallback for enabling APP_STORE_CONNECT -> APPLE_API credential mapping (true/false)
For faster mobile CI builds (mirroring robrix#729), you can pass Cargo profile overrides:
env:
MAKEPAD_MOBILE_CARGO_EXTRA_ARGS: >-
--config profile.dev.opt-level=0
--config profile.dev.debug=false
--config profile.dev.lto=off
--config profile.dev.strip=true
--config profile.dev.debug-assertions=falseThe action installs the tools it needs, so you do not have to add cargo install steps.
cargo-makepad (mobile builds only) is installed from the git repo and revision your
Cargo.lock pins for makepad-widgets, so the build tool always matches the makepad your
app compiles against, forks included. The lookup walks up from project_path to find the
nearest Cargo.lock, so workspace members work too. An already-installed cargo-makepad is
reused only when its revision matches; otherwise it is reinstalled. If makepad-widgets is
not a git dependency, or no lockfile is found, it falls back to upstream makepad/makepad
branch dev and any installed copy is accepted.
robius-packaging-commands (desktop builds only) is installed from crates.io, so a
pinned version is exact and immutable. With robius_packaging_commands_version set, that
version is installed even if another is already on PATH. With it unset, an existing copy on
PATH is reused untouched, and only an absent tool triggers an install of the latest
published version.
cargo-packager (desktop builds only) is installed from crates.io at its latest version
when absent, and an existing copy on PATH is reused.
artifacts: JSON array of{ path, platform, arch, mode, version }with absolute paths. This lists everything built, including artifacts the release step later filters out.app_name: resolved app name; omitted if it cannot be resolvedapp_version: resolved version; omitted if it cannot be resolvedrelease_id: GitHub Release ID used for upload (if any)release_url: GitHub Release URL, set both when the action creates a release and when it uploads to an existingreleaseId
- Read inputs and resolve app metadata from
Cargo.tomlunless overridden. - Determine the target from
args(--target), else default to the host platform. Mobile builds require a target triple (e.g.aarch64-linux-android,aarch64-apple-ios), and OpenHarmony targets fail fast. - Install the packaging tools for that target (see Packaging tool versions).
- Build the release build, then the debug build if
include_debugis on, and collect the artifacts into a normalized list. - If
verify_deb=true, verify every built.debbefore any upload, so a package with a missing runtime dependency never reaches a release. - Upload to the release named by
releaseIdortagName, writelatest.json, and finally upload to TestFlight if enabled.
- Android package names are normalized to valid Java identifiers (e.g.
dora-studio→dora_studio) - Desktop artifacts are collected from
[package.metadata.packager].out_dir(default<root>/dist) by file extension, with no filename filtering, so unrelated files with a packaged extension sitting in that directory are picked up too - Before packaging with an explicit
--target, the action fails fast if yourbefore-each-package-commandpoints--path-to-binaryat an untripledtarget/release/path, since that mismatch otherwise packages a stale or missing binary - If
releaseIdprovided, upload artifacts to that release (no release creation) - If
tagNameprovided (andreleaseIdnot set), create/update a GitHub Release and upload artifacts - Concurrent jobs sharing a
tagNameare handled by locking on the tag ref, waiting for the release list to settle, and cleaning up duplicate releases that carry no unique assets. Passing an explicitreleaseIdis still the more predictable pattern for large matrices. - Supports publishing to another repository via
owner+repo(token must have permission there) - Supports GitHub Enterprise/self-hosted API URLs via
githubBaseUrl - Release upload groups artifacts by platform/arch/mode and, when a group contains a
recommended format, drops the rest of that group. Recommended formats are macOS
.dmg/.pkg, Windows.msi/.exe, Linux.deb/.appimage/.rpm, Android.apkand iOS.ipa. This is why an iOS.appis not uploaded once an.ipaexists. - If an artifact is a directory (like
.app), it is zipped before upload - Asset names default to a unique
app-version-platform-arch-mode.extpattern unless overridden. Colliding names are numbered, and an existing release asset with the same name is replaced. - When
uploadUpdaterJson=true, release upload creates/updates alatest.jsonasset (version,notes,pub_date,platforms). Any existinglatest.jsonon the release is merged rather than overwritten, so matrix jobs accumulate platform entries instead of clobbering each other. - For draft releases, updater URLs are generated using the release tag (
/releases/download/<tag>/<asset>) and become publicly downloadable after the release is published latest.jsonentries are mapped only from.msi, Windows.exe,.appimage,.app,.apk,.ipaand macOS.tar.gz. Formats like.dmg,.pkg,.deband.rpmhave no updater mapping, so a macOS or Linux release that ships only those produces no updater entry for that platform.- If
<artifact>.sigexists next to an uploaded artifact anduploadUpdaterSignatures=true, it is uploaded as<asset>.sigand used assignatureinlatest.json - Desktop entries require signatures in
latest.json; mobile entries (apk/ipa) are allowed withoutsignature - Release upload requires a token with
contents: writepermission
A .deb declares its runtime dependencies, but they cannot be fully derived by
static analysis: dpkg-shlibdeps sees only linked libraries, so anything loaded
via dlopen (OpenGL/EGL, D-Bus) or spawned as a program (xdg-open) is invisible
to it. A package can therefore install cleanly and then fail to start.
Setting verify_deb: true runs robius-packaging-commands verify-deb on every
built .deb before any artifact is uploaded. It installs the package into a
minimal container using only its declared Depends (which also proves
installability), boots the app under strace, and fails if the app loads a
library or spawns a program that no declared dependency provides. On failure it
prints the exact packages that are missing and the command that adds them.
robius-packaging-commands is installed by the desktop build, so no extra step is needed:
- uses: project-robius/makepad-packaging-action@v1.7.0
with:
packager_formats: deb
releaseId: ${{ needs.create_release.outputs.release_id }}
verify_deb: trueAny .deb build on Linux also installs apt-file and refreshes its index first (roughly
20s and 300MB), so the dependency resolver can identify the package owning a dlopen'd
library from the archive's file lists instead of only what happens to be installed on the
runner. It is best-effort and logs a warning if it fails.
Container mode needs a container engine on the runner (GitHub-hosted Linux
runners have Docker preinstalled). Pass --host via verify_deb_args to check
against the runner itself instead, which needs no container but is a weaker test.
While rolling this out, verify_strict: false reports problems as warnings
without blocking uploads:
verify_deb: true
verify_strict: falseNote that verification observes only the code paths a short boot exercises, so a pass means the startup-critical dependencies are present, not that the dependency list is provably complete.
For a CI build the action runs cargo makepad apple ios ... run-device (or run-sim for
simulator builds) with --device=iPhone, then locates the built .app bundle. A missing
bundle is reported as a warning, not a failure, so check the artifact list if a later step
finds nothing to package.
Common cargo-makepad commands, if you want to reproduce a build locally:
# Install toolchain
cargo makepad apple ios install-toolchain
# Run on simulator
cargo makepad apple ios --org=org.example --app=MyApp run-sim -p my-app --release
# Run on device (requires provisioning profile)
cargo makepad apple ios --org=org.example --app=MyApp run-device -p my-app --release
# List certificates/profiles/devices
cargo makepad apple listiOS device builds require a provisioning profile. Create an empty app in Xcode with the
same organization and product names you plan to use (no spaces or unusual characters),
then run it on a real device at least once so the profile is generated. Use those values
for MAKEPAD_IOS_ORG and MAKEPAD_IOS_APP.
If you have multiple signing identities or profiles, set MAKEPAD_IOS_PROFILE and
MAKEPAD_IOS_CERT (or provide APPLE_SIGNING_IDENTITY so the action can select the right cert).
To upload to TestFlight, set upload_to_testflight=true (or MAKEPAD_IOS_UPLOAD_TESTFLIGHT=true) and provide:
APP_STORE_CONNECT_API_KEY(orAPP_STORE_CONNECT_API_KEY_CONTENT)APP_STORE_CONNECT_KEY_IDAPP_STORE_CONNECT_ISSUER_ID
When TestFlight upload is enabled, the action requires a device build (MAKEPAD_IOS_SIM=false)
and automatically forces MAKEPAD_IOS_CREATE_IPA=true.
APP_STORE_CONNECT_API_KEY_CONTENT is usually plain multi-line PEM text. If you prefer storing base64 in secrets, set APP_STORE_CONNECT_API_KEY_CONTENT_BASE64 (or APP_STORE_CONNECT_API_KEY_BASE64).
For macOS desktop packaging, cargo-packager can use:
APPLE_CERTIFICATE+APPLE_CERTIFICATE_PASSWORDfor signing certificate import (same pair reused by iOS device signing)- notarization credentials via one of:
APPLE_KEYCHAIN_PROFILEAPPLE_ID+APPLE_PASSWORD+APPLE_TEAM_IDAPPLE_API_KEY+APPLE_API_ISSUER+APPLE_API_KEY_PATH
If enable_macos_notarization=true (or MAKEPAD_MACOS_ENABLE_NOTARIZATION=true) and explicit macOS notarization env vars are not set, this action automatically reuses:
APP_STORE_CONNECT_API_KEY(_CONTENT)APP_STORE_CONNECT_KEY_IDAPP_STORE_CONNECT_ISSUER_ID
It writes a temporary AuthKey_<KEY_ID>.p8 file and maps them to APPLE_API_* for cargo-packager.
DMG creation retries after detaching mounted volumes and deleting stale .dmg files, so
hdiutil "resource busy" messages followed by a retry in the log are expected recovery, not
a failure.
When tagName or releaseName contains __VERSION__, it is replaced with the resolved app version.
- You do not pass signatures in a dedicated action input.
- This action does not sign anything. Produce the
.sigfiles yourself, typically withcargo packager signer sign, and make them available beside the built artifacts. - Place signature files beside built artifacts using the
<artifact>.signaming convention. - Example: if an uploaded asset resolves to
robrix-1.2.3-windows-x86_64-release.exe, provide a file ending with.exe.sigfor that artifact source path. - With
uploadUpdaterSignatures=true(default), the action uploads those.sigfiles and writes them intolatest.jsonunder each matched platform entry. - Desktop entries without a corresponding
.sigare skipped fromlatest.json.
Use one of these patterns depending on workflow size:
Simple mode(single job / quick setup): call this action once withtagName(orreleaseId) and let it build + upload in one step. This is useful when you want minimal YAML and fast setup.Robust matrix mode(recommended for many parallel jobs): create the GitHub Release once, pass itsreleaseIdinto each build job, and let each job upload only to that existing release. This avoids release-creation races and keeps multi-platform uploads consistent.Build-only mode: omit bothtagNameandreleaseIdif you only want artifacts from the build step and will handle release publishing elsewhere.
For iOS device builds, supply certificate and provisioning profile via env vars.
When MAKEPAD_IOS_PROFILE/MAKEPAD_IOS_CERT are omitted, the action will install and extract them.
- uses: project-robius/makepad-packaging-action@v1.7.0
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_PROVISIONING_PROFILE: ${{ secrets.APPLE_PROVISIONING_PROFILE }}
APPLE_KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }}
with:
args: --target aarch64-apple-ios- uses: project-robius/makepad-packaging-action@v1.7.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tagName: app-v__VERSION__
releaseName: "App v__VERSION__"
releaseBody: "See the assets to download this version and install."
releaseDraft: true
prerelease: false
args: ${{ matrix.args }}Create the release once, then pass its ID to every build job so assets land on the same page.
jobs:
create_release:
runs-on: ubuntu-22.04
outputs:
release_id: ${{ steps.create_release.outputs.id }}
steps:
- uses: softprops/action-gh-release@v2
id: create_release
with:
tag_name: v1.2.3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
package:
needs: create_release
runs-on: ubuntu-22.04
steps:
- uses: project-robius/makepad-packaging-action@v1.7.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
releaseId: ${{ needs.create_release.outputs.release_id }}
args: --target aarch64-linux-android- uses: project-robius/makepad-packaging-action@v1.7.0
with:
args: --target aarch64-linux-androidMore complete workflows, including desktop matrices, debug+release builds, custom asset
naming and iOS TestFlight, are in examples/.
dist/index.js is the committed bundle that GitHub actually runs, so editing src/
without rebuilding ships no change at all. After any source edit:
bun install && bun run build # or: npm install && npm run build
git add dist/Bump version in package.json in the same commit, then tag the release.
- Desktop packaging: implemented (cargo-packager)
- Android packaging: implemented (APK build)
- iOS packaging: implemented (app bundle, optional IPA)
- OpenHarmony packaging: not implemented
- Web packaging: not implemented yet
- Release upload: implemented
- Web packaging