Guides
Shipping
Builds for the App Store and Google Play, a signed and notarized Mac app, a Windows folder, a Linux archive and Flatpak manifest, and what asuka.json says of the app.
asuka build makes an app's release build for one platform, from the
app's directory (or the one given), into its dist. asuka publish
uploads the iOS and Android builds to their stores: to TestFlight and
App Review, or a Google Play track (see Publishing).
--platform |
What it makes, in dist |
Builds on | Given to users |
|---|---|---|---|
ios |
<Name>.ipa, signed for the App Store |
a Mac with Xcode | TestFlight, the App Store (asuka publish) |
android |
<Name>.aab, signed with your upload key |
any system with a JDK, the Android SDK and the NDK | Google Play (asuka publish) |
mac |
<Name>.app, universal; or a .pkg (--appstore) |
a Mac with Xcode's command line tools | a download, signed and notarized; the Mac App Store |
windows |
<Name>/, the .exe and its assets |
any system | a zip, or an installer or MSIX of yours |
linux |
linux/: an install prefix, a .tar.gz, a Flatpak manifest |
any system | the .tar.gz, a Flatpak |
web |
web/, a static site |
any system | any web server or CDN |
Without --platform, on a Mac, a Windows PC or a Linux machine, it
builds for that computer. Every build but iOS's and Android's is plain
Go with CGO_ENABLED=0, so it needs no C toolchain.
asuka.json
{
"name": "Notes",
"id": "com.example.notes",
"version": "1.2",
"build": 7,
"icon": "icon.png",
"ios": { "team": "ABCDE12345" },
"android": { "keystore": "upload.jks", "keyAlias": "upload" }
}Every key is optional; these apply on every platform:
nameis the app's name: under the icon, in the Mac's menu bar, the window's title on Windows, the launcher's on Linux, the web page's title. It names the build's files too (Notes.app,Notes.exe). The default is the directory's name.idis the bundle ID on iOS and the Mac, the application ID on Android and Linux (its desktop file, icon and D-Bus name), the AppUserModelID on Windows (its toasts'). The default isdev.asuka.<directory>.versionis what users see (default1.0);buildcounts the builds uploaded, and must grow with each (default 1).asuka publishnumbers each build after the store's highest itself, sobuildis only where it starts. On Windows they're the.exe's file version (1.2.0.7for version 1.2 and build 7), on Linux the metainfo's release.iconis a square PNG, 1024×1024, without transparency for iOS. Each build makes the sizes its platform wants: an asset catalog on iOS, the launcher's on Android, an.icnson the Mac, an.icoin the.exeon Windows, the hicolor theme's from 16 to 512 pixels on Linux, the web manifest's, the favicon and iOS's home screen icon on the web.accentis the app's accent color,"#rrggbb": the tint on iOS, the seed of the Material 3 color scheme on Android, the color ofAccent, filled buttons and links on the Mac (AppKit's own controls keep the accent the user picks), the controls' accent on Windows in place of the system's, libadwaita's accent on Linux, and on the web the page's accent and its manifest'stheme_color. Without it, each platform's own.schemeopens the app formyapp://URLs: on iOS, Android, the Mac (CFBundleURLTypes), Linux (the desktop file'sx-scheme-handler) and Windows (the app registers it for the user as it starts, inHKEY_CURRENT_USER\Software\Classes).linksopens it for https links to your domains (universal links, App Links; iOS and Android).ui.OnOpenURLgets them.
Platform by platform:
ios.*is the iOS build's (iOS);ios.teamis the Mac build's team too, andios.usage's purpose strings go in the Mac's Info.plist as well.android.*is the Android build's (Android).mac.profileis a Mac build's provisioning profile, andmac.appStoreProfilethe Mac App Store's: see macOS.linux.*is what a Linux build's desktop file and AppStream metainfo say: see Linux.web.vapidPublicKeyis the app server's VAPID public key, forasuka/pushon the web (Web Push; see the web).- A Windows build needs nothing beyond the keys above; its signing certificate is given in the environment (Windows).
These are for the phones and tablets:
orientations:portrait,landscape(both by default), on phones. On iPad the app takes every orientation, as Split View, Slide Over and Stage Manager need; Android 16 likewise ignores it on tablets and foldables.ios.iphoneOnlyleaves iPad out. By default the app is an iPad app too, at whatever size the user gives its window; on Android it's resizable for split screen and desktop windows. Lay out fromui.UseEnvironment()'sWidthandHeight, the window's size, not the device's.shareTargetoffers the app in other apps' share sheets:truefor texts and links, or{"types": ["text", "image/*"], "max": 10}for files too (see Device);ui.OnSharegets them. On iOS it's a share extension in the app, with a bundle ID of its own (the app's plus.share).splashis the launch screen:{"image": "logo.png", "size": 96, "background": "#ffffff", "darkBackground": "#000000"}(see Device).widgetsare the app's home screen widgets andshortcutsits actions for Siri, Spotlight and the launcher (see Device). The widget extension has a bundle ID of its own (the app's plus.widgets): a release needs a profile for it too, andios.widgetProfilenames one.android.googleServicesis the Firebase project'sgoogle-services.json, forasuka/push;android.mapsKey, a Google Maps Platform API key, forasuka/maps.ios.usagegives the purpose strings of protected resources (NSCameraUsageDescription...; the Mac's too), andios.infoPlistadds keys to Info.plist;ios.backgroundModesadds toUIBackgroundModes("audio", to record and play with the screen locked);android.permissionsadds permissions.
The packages an app imports give it the permissions and purpose strings
they need: asuka/camera the camera's, asuka/location the location's.
Write your own in ios.usage: the default is generic. Some need a
capability on the App ID too (Push Notifications for asuka/push, Sign
in with Apple for asuka/appleauth, App Attest for asuka/integrity,
Declared Age Range for asuka/agerange): turn it on at
developer.apple.com, then make the profile again. asuka build --platform ios (and
--platform mac) says which a profile lacks.
iOS
asuka build --platform iosmakes a .ipa signed for the App Store, with the team's distribution
certificate (in your keychain, from Xcode's Accounts) and a provisioning
profile for the app's ID: ios.profile, or --profile, else the best
match installed. --distribution ad-hoc or development signs it for the
devices of its profile instead, and --simulator makes an unsigned .app
for the simulator.
With shareTarget, the share extension is signed too, with a profile
for its own ID (com.example.notes.share for com.example.notes), from
the app's team: ios.shareProfile, else the best match installed. A
wildcard profile (com.example.*) serves both. Register the extension's
ID at developer.apple.com and make it a profile as you did the app's.
A shareTarget that takes files also needs the App Group
group.<app ID>, through which the extension hands the files to the
app. At developer.apple.com, register the group, give both IDs (the
app's and the extension's) the App Groups capability with it, then make
their profiles again. A wildcard ID can't have App Groups. The build
stops, saying so, if a profile lacks the group. The simulator needs
none of this. An app with its own share sheet (app.ShareSheet) needs
the group too, for its data, whatever it takes.
asuka publish --platform ios makes it and uploads it to App Store Connect (see
Publishing); Apple's Transporter uploads one too.
Privacy manifest. The App Store requires one, giving a reason for
each API of Apple's list the app uses. Asuka writes it, from what the
binary uses: Go's runtime reads the clock and file times, SQLite the disk
space; it declares no tracking and no data collected. If your app
collects data or tracks, write your own as ios/PrivacyInfo.xcprivacy,
with those reasons in it too; Asuka uses it instead.
Android
A release is signed with your upload key. Make one, once, and keep it safe:
keytool -genkeypair -v -keystore upload.jks -alias upload -keyalg RSA -keysize 2048 -validity 10000Name it in asuka.json (android.keystore, android.keyAlias), and give
its password in the environment:
ASUKA_KEYSTORE_PASSWORD=... asuka build --platform android(ASUKA_KEY_PASSWORD too if the key's differs; ASUKA_KEYSTORE and
ASUKA_KEY_ALIAS name them in CI.) It makes an .aab for Google Play, for
64-bit and 32-bit ARM phones and x86_64 (emulators, Chromebooks),
Android 13 and later; Play makes each phone's APK from it, with only
that phone's architecture. asuka publish --platform android makes it and uploads it to Google Play (see
Publishing).
Publishing
asuka publish makes the release build, as asuka build does, and
uploads it through the store's API. It asks the store first what could
stop it (that the app is there and the key may upload to it, that the
version still takes builds, that the TestFlight groups exist), so
nothing is built for an upload that would fail. Each build is numbered
one above the store's highest (and at least asuka.json's build):
there's nothing to bump between releases. --build 42 gives one.
App Store Connect. Make an API key in App Store Connect (Users and
Access › Integrations › App Store Connect API) with the App Manager
role, download its .p8, and name it in asuka.json, or in the
environment (ASUKA_ASC_KEY_ID, ASUKA_ASC_ISSUER_ID, and
ASUKA_ASC_KEY, the .p8's path or its text). Without a key, it's
looked for as altool does, AuthKey_<id>.p8 in
~/.appstoreconnect/private_keys. Keep the .p8 out of your
repository. Add the app in App Store Connect first (Apps › + › New
App), with its bundle ID.
"ios": {
"appStoreConnect": { "keyId": "ABC123DEF4", "issuerId": "57246542-96fe-1a63-e053-0824d011072a" }
}asuka publish --platform ios # to TestFlight: the team's testers
asuka publish --platform ios --groups "Friends, QA" --notes "Try the new sync."
asuka publish --platform ios --review --notes @release-notes/ios --release manualIt uploads the .ipa, waits for Apple to process it (minutes, usually;
at times an hour or more), printing what Apple warns of or turns down,
then:
--notesis TestFlight's What to Test and, with--review, the App Store's What's New: text, or@file, in the app's primary language; or@dir, a file per language, named as App Store Connect names it (en-US.txt,ja.txt). An update needs What's New in each of the version's languages.--groupsgives the build to TestFlight groups, by name. With an external group (testers outside your team), the build goes to beta review; fill in the test information in TestFlight first.--reviewmakes the App Store version (asuka.json'sversion) if there isn't one, gives it the build, and submits it for review. A first submission needs its listing, screenshots and privacy answers filled in App Store Connect first; what's missing is printed.--release autoreleases it once approved,--release manualwaits for you.--no-waitstops once Apple has the upload.
Google Play. Make a service account in Google Cloud, with a JSON key
(IAM › Service accounts › Keys), in a project with the Google Play
Android Developer API turned on. Invite its email in the Play Console
(Users and permissions), with the rights to release the app. Name its
key in asuka.json ("android": {"serviceAccount": "play.json"}) or in
ASUKA_PLAY_SERVICE_ACCOUNT (its path or its JSON). Play takes an app's
first .aab only in the Play Console: create the app there and upload
its first build by hand; asuka publish uploads the ones after.
asuka publish --platform android # the internal test track
asuka publish --platform android --track beta --notes @release-notes/android
asuka publish --platform android --track production --rollout 0.1--trackisinternal(default),alpha(closed testing),beta(open testing),production, or a closed track's name.--rollout 0.1rolls the release out to a tenth of users; raise it in the Play Console.--draftmakes a draft, to roll out there. Until the app is published, Play takes drafts only: the release becomes one.--notesare the release notes (at most 500 characters): text or@filein the listing's default language, or@dirwith a file per language as Play names it (en-US.txt,ja-JP.txt).- If the app has changes in review, it stops;
--cancel-reviewcancels that review and sends them with this release. If Play holds the app's changes for you to send (after a rejection), they're saved, and you send them in the Play Console.
--file dist/Notes.ipa (or .aab) uploads a build asuka build made,
instead of making one.
macOS
asuka build --platform macmakes dist/<Name>.app on a Mac: a universal app (Apple silicon and
Intel), for macOS 11 and later, with its Info.plist, icon, languages and
assets, signed ad hoc: it runs on your Mac. The packages the app imports
give it their purpose strings, as on iOS, from ios.usage (location's
is NSLocationUsageDescription on the Mac, taken from
NSLocationWhenInUseUsageDescription), and the hardened runtime's
entitlements for them (com.apple.security.device.camera,
...audio-input, ...personal-information.location...), without which
an app signed with a Developer ID is refused the camera, the microphone
or the user's data. An app with Swift of its own (macos/*.swift) has it
compiled for both architectures into Contents/Frameworks, signed with
the app (see Native code).
To give it to others outside the Mac App Store, sign it with your Developer ID and have Apple notarize it, so Gatekeeper opens it:
xcrun notarytool store-credentials asuka # once: your Apple ID, team and an app-specific password
asuka build --platform mac --sign "Developer ID Application: Your Name (TEAMID)" --notarize asuka--notarize sends the signed app to Apple's notary service, waits for
its verdict (a few minutes, usually), prints the service's log if it's
rejected, and staples the ticket to the app, so it opens offline too.
In CI, --notarize env takes ASUKA_APPLE_ID, ASUKA_APPLE_PASSWORD (an
app-specific password) and ASUKA_TEAM_ID from the environment instead
of a keychain profile.
Push (asuka/push), Sign in with Apple (asuka/appleauth) and App Attest
(asuka/integrity) need entitlements only a provisioning profile grants.
Make a macOS Developer ID profile for the app's ID at developer.apple.com
(Profiles), with those capabilities on for the ID, and download it:
asuka build --platform mac --profile Notes.provisionprofile --notarize asukaThe profile goes in the app as Contents/embedded.provisionprofile, and
the app is signed with what its packages need of the profile's
entitlements (com.apple.developer.aps-environment,
com.apple.developer.applesignin,
com.apple.developer.devicecheck.appattest-environment), its app ID and
team, and the hardened runtime. The build says which a profile lacks.
Without --sign, the identity is the keychain's certificate the profile
allows (Developer ID Application); a --sign the profile doesn't list is
an error. --profile takes a path, or an installed profile's name or UUID;
ASUKA_MAC_PROFILE and asuka.json's mac.profile say the same. With none,
an app that uses those packages is signed with the best match installed
for its ID (in Xcode's profile folders), else without one: the build
says what won't work on the Mac, and the rest of the app does.
--appstore makes dist/<Name>.pkg for the Mac App Store instead: the app in the
App Sandbox (with the network and the files the user picks), signed
with a Mac App Store profile's certificate (Apple Distribution;
asuka.json's mac.appStoreProfile, else --profile, else the best match),
and the package with the team's Mac Installer Distribution certificate.
Upload it with Transporter or xcrun altool --upload-package;
asuka publish doesn't upload Mac builds yet. Notarizing is for apps
outside the store: --notarize with --appstore is an error.
Windows
asuka build --platform windows # x64, which Windows on Arm runs emulated
asuka build --platform windows --arch arm64 # native on Arm PCsmakes a folder, dist/<Name>, from any system: <Name>.exe and the
app's assets beside it. The .exe is a GUI program (no console window)
with its resources built in: the icon at Windows's sizes, its version
information (version and build), and a manifest that asks for the
common controls 6, per-monitor DPI awareness and UTF-8, for Windows 10
and 11. An app with a .syso of its own in its directory has its own
resources instead. Zip the folder, or package it (an installer, MSIX).
Sign the .exe, so SmartScreen knows who it's from, with a code signing
certificate exported as a .pfx:
ASUKA_WINDOWS_CERT=cert.pfx ASUKA_WINDOWS_CERT_PASSWORD=... asuka build --platform windowsThe build signs it with SHA-256 and has the signature timestamped, so
it stays valid once the certificate expires, by the first RFC 3161 server
that answers: ASUKA_WINDOWS_TIMESTAMP's (a comma-separated list), else
DigiCert's, Sectigo's and GlobalSign's. When none answers the build
fails, saying so; ASUKA_WINDOWS_TIMESTAMP=none signs without a timestamp
(the signature then ends with the certificate). It signs with signtool on
Windows (the Windows SDK's; on the PATH, else the newest installed),
with osslsigncode elsewhere (brew install osslsigncode, apt install osslsigncode). Without ASUKA_WINDOWS_CERT the .exe isn't signed. A
certificate on a hardware token or in a cloud service (Azure Trusted
Signing) signs through its own tool: sign the .exe in the folder with
it after the build.
An app that shows notifications (asuka/notify) is given a shortcut in the
user's Start menu the first time, which Windows needs for an app's
toasts: its AppUserModelID is asuka.json's id (else one of its name).
Linux
asuka build --platform linux # this machine's architecture, else amd64
asuka build --platform linux --arch arm64 # amd64 or arm64It builds from any system: the host calls GTK 4 and libadwaita without
cgo. In dist/linux it makes:
-
<name>/, a folder laid out as an install prefix, named after the app in lower case (My Notes:my-notes):bin/my-notes, the app;share/my-notes/assets, its assets, which it finds frombin/;share/applications/<id>.desktop, what launchers show and run;share/icons/hicolor/<size>/apps/<id>.png, theiconat each size from 16 to 512 pixels;share/metainfo/<id>.metainfo.xml, its AppStream metainfo, what software centers show.
Run
bin/my-notesfrom it, or install it into a prefix, where launchers find it:tar -xzf my-notes-1.0-linux-amd64.tar.gz --strip-components=1 -C ~/.local -
my-notes-<version>-linux-<arch>.tar.gz, that folder, to hand out. -
<id>.yml, a Flatpak manifest that packages that folder on the GNOME runtime:flatpak install flathub org.gnome.Platform//50 org.gnome.Sdk//50 # once flatpak-builder --user --install --force-clean build dist/linux/com.example.notes.yml flatpak run com.example.notes
The app needs GTK 4.14 and libadwaita 1.5 or later where it runs (the
GNOME runtime 46 or later has them). Its ID is its application ID: it
names its desktop file, its icon and its D-Bus name (a second launch
shows the first's window), and its data directory
(~/.local/share/<id>); it must be letters, digits, _ and -, in two
parts or more, none starting with a digit.
What the desktop file and metainfo say, in asuka.json:
"linux": {
"summary": "Write things down",
"description": "Notes, kept on your computer.\n\nA second paragraph.",
"categories": ["Office"],
"keywords": ["memo"],
"license": "MIT",
"developer": "Example Inc.",
"homepage": "https://example.com",
"flatpakRuntime": "50"
}categories are freedesktop.org's
(Utility by default); license is an SPDX expression; Flathub and
software centers want the summary, description, license and developer.
The web
asuka build --platform webmakes a static site in dist/web: index.html, the app as
WebAssembly, the web host's scripts, the app's service worker, its
assets, a web app manifest with its icons, gzipped copies, and a
_headers file for Cloudflare and Netlify. The files are named by their
digests, so all but index.html and the manifest can be cached for
good. Serve the folder from any web server or CDN, .wasm as
application/wasm, over HTTPS (service workers, Web Push and the secure
store need it). See the web for what's in it and what a
browser does differently.
In an existing app
A Asuka app can live inside an app made in Xcode or Android Studio: a feature written once in Go, shown from a native app. (On the web, the build's app runs in an element of a page of yours: see the web.)
iOS. asuka build --platform ios --xcframework makes dist/AsukaApp.xcframework:
the app's Go code and the UIKit host, for devices and the simulator, with
a Swift module interface and the app's assets. Add it to your target
(Embed & Sign), then show the Asuka app as any view controller:
import AsukaApp
present(AsukaViewController(), animated: true)
// or push it, or make it a tab, or a window's rootAndroid. asuka build --platform android --aar makes a Maven repository in
dist/maven: the host library with the app's Go code for each ABI, its
assets, and AsukaActivity declared, with its dependencies (Material,
AndroidX) in its POM, and the optional modules the app uses (asuka/push,
asuka/maps) beside it. Add the repository and the dependency the
command prints, then start the activity:
// settings.gradle.kts
dependencyResolutionManagement { repositories { maven { url = uri("path/to/dist/maven") } } }
// app/build.gradle.kts
dependencies { implementation("dev.asuka.counter:counter:1.0.0") }
// anywhere
startActivity(Intent(this, dev.asuka.AsukaActivity::class.java))The embedded app is one Asuka app, mounted once, as in a standalone
build. What asuka.json sets for a whole app (its icon, URL scheme,
share target, widgets, permissions' texts) is the embedding app's to
set in its own project.
Symbols for crash reports
A native crash's frames are addresses in the app's binaries; a crash service (or App Store Connect, or Google Play) turns them into functions and lines with the build's symbols. Go's own stack traces need none (their tables are in the binary), so a Go panic reads as it is.
iOS. asuka build --platform ios compiles the host with debug information,
keeps the Go code's, and makes a dSYM of each binary (the app, its
framework when it has a share sheet, its extensions), as Xcode's archive
does, then strips the binaries. They're in dist/<Name>.dSYMs, beside
the .ipa (or the simulator's .app), and their symbols in the .ipa
(Symbols/), so App Store Connect and Xcode's Organizer symbolicate
the crashes they show. Upload the dSYMs to your crash service:
sentry-cli debug-files upload --org ORG --project PROJECT dist/Notes.dSYMs
# Crashlytics' tool, from its SDK:
upload-symbols -gsp GoogleService-Info.plist -p ios dist/Notes.dSYMsAndroid. asuka build --platform android keeps the Go libraries' symbol tables
for Gradle, which strips the libraries it packages and puts their
symbols in the .aab, with R8's map of the Kotlin it renamed: Google
Play symbolicates native crashes and ANRs, and deobfuscates stack
traces, with them. For a crash service they're beside the .aab too:
dist/<Name>-native-debug-symbols.zip and dist/<Name>-mapping.txt.
sentry-cli debug-files upload --org ORG --project PROJECT dist/Notes-native-debug-symbols.zip
sentry-cli upload-proguard --org ORG --project PROJECT dist/Notes-mapping.txtThe Mac, Windows, Linux and the web. The app is one Go binary, built
without its symbol table and debug information (-s -w); Go's stack
traces keep their function names and lines, so its panics read as they
are, and there are no symbols to upload.
Keep each release's symbols: a crash is symbolicated with those of the build it happened in. See Device for the reports themselves.
Before you release
Run the release build where your users will, before you release it:
- On a phone: an iOS build with
--distribution development, and the Android build from Play's internal test track (asuka publish --platform android --track internal), which installs the APK Play makes from the.aab. TestFlight and a Play internal test also check what a simulator can't. - On the Mac: the signed, notarized
.app, downloaded on another Mac (a download is what Gatekeeper checks);spctl --assess --type execute -vv Notes.appsays what Gatekeeper will make of it. - On Windows and Linux: the folder, or the
.tar.gzor Flatpak, on a machine that has never had the app (Linux: with GTK 4.14 and libadwaita 1.5 or later). - On the web: the built site served over HTTPS, in each browser you support.
In CI
This GitHub Actions workflow builds both apps when you push a tag like
v1.2, uploads the iOS build to TestFlight and the Android build to
Play's internal test track with asuka publish, and attaches the .ipa
and the .aab to a draft GitHub release for the tag, each as soon as
it's uploaded; publish
the draft when you're ready. (Run by hand, it keeps them as the run's
artifacts only.) It expects the app at the root of its repository. Do
your first uploads by hand: Play won't take one through its API until
the app has had one.
# .github/workflows/release.yml
name: release
on:
push:
tags: ["v*"]
workflow_dispatch:
permissions:
contents: write # to attach the builds to the tag's release
jobs:
release:
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- run: gh release create "$GITHUB_REF_NAME" --draft --generate-notes --verify-tag
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
ios:
needs: release
if: ${{ !failure() && !cancelled() }} # and when run by hand, which skips release
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version-file: go.mod }
- name: the asuka CLI, at the app's version of asuka
run: go install github.com/egoist/asuka/cmd/asuka@$(go list -m -f '{{.Version}}' github.com/egoist/asuka)
- name: the distribution certificate and the profile
env:
CERTIFICATE: ${{ secrets.IOS_CERTIFICATE }}
CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
PROFILE: ${{ secrets.IOS_PROFILE }}
run: |
KC=$RUNNER_TEMP/signing.keychain-db
PW=$(openssl rand -hex 16)
security create-keychain -p "$PW" "$KC"
security set-keychain-settings -lut 3600 "$KC"
security unlock-keychain -p "$PW" "$KC"
echo "$CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12
security import $RUNNER_TEMP/certificate.p12 -k "$KC" -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -k "$PW" "$KC"
security list-keychains -d user -s "$KC" $(security list-keychains -d user | tr -d '"')
echo "$PROFILE" | base64 --decode > $RUNNER_TEMP/app.mobileprovision
# With shareTarget, the extension's profile, installed where asuka looks:
# mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
# echo "$SHARE_PROFILE" | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/share.mobileprovision
- name: build, and upload to TestFlight
env:
ASUKA_ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASUKA_ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASUKA_ASC_KEY: ${{ secrets.ASC_KEY }}
run: asuka publish --platform ios --profile $RUNNER_TEMP/app.mobileprovision --no-wait
- uses: actions/upload-artifact@v4
with: { name: ios, path: dist/*.ipa }
- name: attach to the tag's release
if: startsWith(github.ref, 'refs/tags/')
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: gh release upload "$GITHUB_REF_NAME" dist/*.ipa --clobber
android:
needs: release
if: ${{ !failure() && !cancelled() }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version-file: go.mod }
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: 17 }
- uses: android-actions/setup-android@v3
with: { packages: platform-tools }
- name: an NDK
run: sdkmanager "ndk;27.1.12297006"
- name: the asuka CLI, at the app's version of asuka
run: go install github.com/egoist/asuka/cmd/asuka@$(go list -m -f '{{.Version}}' github.com/egoist/asuka)
- name: the upload key
env:
KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }}
run: echo "$KEYSTORE" | base64 --decode > $RUNNER_TEMP/upload.jks
- name: build, and upload to Play's internal test track
run: asuka publish --platform android --track internal
env:
ASUKA_KEYSTORE: ${{ runner.temp }}/upload.jks
ASUKA_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ASUKA_PLAY_SERVICE_ACCOUNT: ${{ secrets.PLAY_SERVICE_ACCOUNT }}
- uses: actions/upload-artifact@v4
with:
name: android
path: dist/*.aab
- name: attach to the tag's release
if: startsWith(github.ref, 'refs/tags/')
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: gh release upload "$GITHUB_REF_NAME" dist/*.aab --clobberIts secrets (Settings › Secrets and variables › Actions), the files in
base64 (base64 -i file | pbcopy):
IOS_CERTIFICATE: your Apple Distribution certificate with its private key, a.p12exported from Keychain Access;IOS_CERTIFICATE_PASSWORDthe password you exported it with.IOS_PROFILE: the app's App Store provisioning profile, from developer.apple.com (withshareTarget,SHARE_PROFILEthe share extension's).ASC_KEY_ID,ASC_ISSUER_IDandASC_KEY(the.p8's text): an App Store Connect API key with the App Manager role, from Users and Access › Integrations.ANDROID_KEYSTORE: the upload key's.jks;ANDROID_KEYSTORE_PASSWORDits password (addASUKA_KEY_PASSWORDif the key's differs).PLAY_SERVICE_ACCOUNT: a Google Cloud service account's JSON key, invited in the Play Console's Users and permissions with the right to release to testing tracks.
The CLI carries the iOS and Android hosts, so it's installed at the
version of Asuka in the app's go.mod. Each build is numbered after the
store's highest, so nothing is bumped before you tag; --no-wait ends
the iOS job once Apple has the upload, not once it's processed. The App Store takes builds
made with a recent Xcode only; if the runner's default is older, pick
one with sudo xcode-select -s /Applications/Xcode_<version>.app.
The desktop
These jobs, added to the same workflow, build the desktop apps for the
tag and attach them to its draft release: the Mac's signed with your
Developer ID and notarized, Windows' for x64 and Arm, signed, and
Linux's for amd64 and arm64. Windows' and Linux's build on Linux, so
one Ubuntu runner makes both. (The app is named Notes here: its files
are named after asuka.json's name.)
mac:
needs: release
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version-file: go.mod }
- name: the asuka CLI, at the app's version of asuka
run: go install github.com/egoist/asuka/cmd/asuka@$(go list -m -f '{{.Version}}' github.com/egoist/asuka)
- name: the Developer ID certificate
env:
CERTIFICATE: ${{ secrets.MAC_CERTIFICATE }}
CERTIFICATE_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
run: |
KC=$RUNNER_TEMP/signing.keychain-db
PW=$(openssl rand -hex 16)
security create-keychain -p "$PW" "$KC"
security set-keychain-settings -lut 3600 "$KC"
security unlock-keychain -p "$PW" "$KC"
echo "$CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12
security import $RUNNER_TEMP/certificate.p12 -k "$KC" -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -k "$PW" "$KC"
security list-keychains -d user -s "$KC" $(security list-keychains -d user | tr -d '"')
- name: build, sign and notarize
env:
ASUKA_APPLE_ID: ${{ secrets.APPLE_ID }}
ASUKA_APPLE_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
ASUKA_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: asuka build --platform mac --sign "Developer ID Application: Your Name (TEAMID)" --notarize env
- name: attach to the tag's release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
ditto -c -k --keepParent dist/Notes.app dist/Notes-mac.zip
gh release upload "$GITHUB_REF_NAME" dist/Notes-mac.zip --clobber
windows-linux:
needs: release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version-file: go.mod }
- name: the asuka CLI, at the app's version of asuka
run: go install github.com/egoist/asuka/cmd/asuka@$(go list -m -f '{{.Version}}' github.com/egoist/asuka)
- name: osslsigncode, and the code signing certificate
env:
CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
run: |
sudo apt-get install -y osslsigncode
echo "$CERTIFICATE" | base64 --decode > $RUNNER_TEMP/cert.pfx
- name: Windows, x64 and Arm
env:
ASUKA_WINDOWS_CERT: ${{ runner.temp }}/cert.pfx
ASUKA_WINDOWS_CERT_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
run: |
asuka build --platform windows -o dist/x64
asuka build --platform windows --arch arm64 -o dist/arm64
(cd dist/x64 && zip -r ../Notes-windows-x64.zip Notes)
(cd dist/arm64 && zip -r ../Notes-windows-arm64.zip Notes)
- name: Linux, amd64 and arm64
run: |
asuka build --platform linux
asuka build --platform linux --arch arm64
- name: attach to the tag's release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: gh release upload "$GITHUB_REF_NAME" dist/*.zip dist/linux/*.tar.gz --clobberTheir secrets: MAC_CERTIFICATE, your Developer ID Application
certificate with its private key (a .p12), and
MAC_CERTIFICATE_PASSWORD; APPLE_ID, APPLE_APP_PASSWORD (an
app-specific password, from account.apple.com) and APPLE_TEAM_ID, for
the notary service; WINDOWS_CERTIFICATE, a code signing certificate
(a .pfx), and WINDOWS_CERTIFICATE_PASSWORD. Each Linux build makes
its own .tar.gz; the Flatpak manifest packages the last one built, for
its architecture.
Languages
An app with catalogs needs nothing more: asuka build declares its
languages to each store and system (CFBundleLocalizations and .lprojs on
iOS and macOS, a locales_config on Android), which makes Settings offer the
app's language. Windows, Linux and web builds declare none: the app
shows the language the user's settings pick from its catalogs. The app's name and its purpose strings can be translated too,
with keys asuka.appName and asuka.NSCameraUsageDescription in the catalogs
(Packaging).