Their trigger is `v*`, which matches `v0.4.0-beta.1`. They guarded `v0.0.0` -- the version floor -- and nothing else, so the first prerelease tag would have published a beta everywhere. Nothing produces one today. The guard is here because the thing that would is `prerelease: true` in .releaserc.yml, a one-line change whose blast radius is four public channels and which nothing in those four files mentions. That is the same argument release.yml's `chore(release):` guard is kept on: cheap, against something a future edit turns on somewhere else entirely. android-apk is the worst of the four twice over. Its APK goes to the *generic* registry, which is readable without credentials so Obtainium can poll a plain URL, so a beta would be offered to every device on it. And its versionCode maths splits on dots: it would read "1" out of "0-beta" and produce a wrong number rather than a failed build, which matters because Android orders releases by that integer and refuses anything not greater than what is installed. Each is a clean skip rather than a failure, matching the v0.0.0 guard beside it: a red run against a tag that was never meant to ship is noise.
492 lines
23 KiB
YAML
492 lines
23 KiB
YAML
name: Build & publish the Android APK
|
|
|
|
# The fifth workflow, and the second that publishes. It builds a signed
|
|
# arm64-v8a APK on every version tag and puts it in
|
|
# Gitea's *generic* package registry, which — unlike the repository — is
|
|
# readable without credentials. That is what lets an Obtainium client
|
|
# poll a plain URL with no token and no public mirror of the source.
|
|
#
|
|
# **Why its own file rather than a job in ci.yml.** `ci.yml` runs on
|
|
# every branch push and is the workflow that gates; this one runs on
|
|
# tags only, takes tens of minutes on a cold cache, and the runner has
|
|
# capacity 1. Hanging it off the gate would put every push behind an
|
|
# SDK download.
|
|
#
|
|
# **Why it is keyed on the tag.** The ljos pipeline this is modelled on
|
|
# computes a version in CI and cuts the release itself, then gates the
|
|
# Android job on `needs.release.outputs.version != ''` with an
|
|
# `always()` whose absence silently kills the manual path. This repo
|
|
# has no release automation — tags are pushed by hand and
|
|
# homebrew-formula.yml already keys on `v*` — so the tag *is* the
|
|
# version and none of that machinery, or its failure modes, is needed.
|
|
#
|
|
# It deliberately does **not** carry `continue-on-error`. In ljos the
|
|
# Android job shared a pipeline with a server deploy that must never go
|
|
# red over a phone build; here it is standalone and can neither delay
|
|
# nor redden anything, so a release step that fails silently would be
|
|
# strictly worse than one that fails visibly.
|
|
|
|
on:
|
|
push:
|
|
tags: ["v*"]
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: "Version to build (default: the latest v* tag)"
|
|
required: false
|
|
|
|
concurrency:
|
|
group: android-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
apk:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 60
|
|
container:
|
|
image: ubuntu:24.04
|
|
# /cache/tool holds the Go toolchain ci.yml already downloads.
|
|
# The other three are this workflow's own and are ~4 GB between
|
|
# them, which is most of its wall clock on a cold run:
|
|
# android-sdk the SDK, the NDK and the platform (~2 GB)
|
|
# gradle GRADLE_USER_HOME — the wrapper distribution and
|
|
# the AGP dependency graph (~700 MB)
|
|
# pnpm-store shared with ci.yml
|
|
# Every path must be inside the runner's `valid_volumes` allowlist:
|
|
# a directory outside it makes the job **fail to start**, rather
|
|
# than silently skipping the mount.
|
|
volumes:
|
|
- /home/logan/docker/gitea/data/runner/cache/tool:/cache/tool
|
|
- /home/logan/docker/gitea/data/runner/cache/android-sdk:/cache/android-sdk
|
|
- /home/logan/docker/gitea/data/runner/cache/gradle:/cache/gradle
|
|
- /home/logan/docker/gitea/data/runner/cache/pnpm-store:/cache/pnpm-store
|
|
env:
|
|
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
|
|
SERVER_URL: ${{ github.server_url }}
|
|
REPO: ${{ github.repository }}
|
|
OWNER: ${{ github.repository_owner }}
|
|
SHA: ${{ github.sha }}
|
|
REF_NAME: ${{ github.ref_name }}
|
|
DEBIAN_FRONTEND: noninteractive
|
|
GO_VERSION: '1.25.0'
|
|
npm_config_store_dir: /cache/pnpm-store
|
|
# The Go half wants the NDK; the Gradle half wants a platform.
|
|
ANDROID_HOME: /cache/android-sdk
|
|
ANDROID_SDK_ROOT: /cache/android-sdk
|
|
GRADLE_USER_HOME: /cache/gradle
|
|
# Pinned, not "whatever sdkmanager installs": newer NDKs have
|
|
# broken the Wails Android build before, and r26d is what plan
|
|
# 015 phase 0 was verified against.
|
|
NDK_VERSION: 26.3.11579264
|
|
# The registry package name. Obtainium watches
|
|
# <server>/api/packages/<owner>/generic/yellowjacket-android/latest/yellowjacket.apk
|
|
PACKAGE_NAME: yellowjacket-android
|
|
|
|
steps:
|
|
# libgtk-4-dev and libwebkitgtk-6.0-dev are here even though
|
|
# nothing in this job builds a desktop app: `wails3` is the task
|
|
# runner the whole Android build goes through, and the CLI links
|
|
# the GTK/WebKit bindings, so `go tool wails3` cannot compile
|
|
# without them. libasound2-dev is oto's `pkg-config -- alsa`
|
|
# probe, for the same reason (the *Android* build uses oboe, not
|
|
# ALSA — this is the host toolchain only).
|
|
- name: System packages
|
|
run: |
|
|
set -eu
|
|
apt-get update -qq
|
|
apt-get install -y -qq --no-install-recommends \
|
|
ca-certificates curl git jq unzip zip \
|
|
build-essential pkg-config \
|
|
libwebkitgtk-6.0-dev libgtk-4-dev libasound2-dev \
|
|
openjdk-21-jdk-headless
|
|
|
|
# By hand rather than actions/checkout: that is a JS action and
|
|
# needs node inside the container before any step has installed
|
|
# it. Same approach as the other four workflows.
|
|
- name: Clone repo at this commit
|
|
run: |
|
|
set -eu
|
|
git clone --quiet \
|
|
"https://x-access-token:${PACKAGE_TOKEN}@${SERVER_URL#https://}/${REPO}.git" /src
|
|
git -C /src checkout --quiet --detach "$SHA"
|
|
git config --global --add safe.directory /src
|
|
git -C /src log --oneline -1
|
|
|
|
# A tag push carries the version in its own name. A manual run has
|
|
# no tag, so it takes the input or falls back to the latest v* tag,
|
|
# which is what a hand-triggered rebuild wants anyway.
|
|
- name: Resolve the version
|
|
id: version
|
|
working-directory: /src
|
|
run: |
|
|
set -eu
|
|
v="${{ inputs.version }}"
|
|
if [ -z "$v" ]; then
|
|
case "$REF_NAME" in
|
|
v*) v="$REF_NAME" ;;
|
|
*) v=$(git describe --tags --abbrev=0 --match 'v[0-9]*' 2>/dev/null || echo "v0.0.0") ;;
|
|
esac
|
|
fi
|
|
v="${v#v}"
|
|
|
|
# v0.0.0 is semantic-release's version floor, not a shipment —
|
|
# see the bootstrap step in release.yml. It is skipped cleanly
|
|
# rather than failing the guard below, because a 45-minute red
|
|
# run against a tag that was never meant to ship is noise, and
|
|
# this is the most expensive of the four workflows a tag fires.
|
|
if [ "$v" = "0.0.0" ]; then
|
|
echo "v0.0.0 is the version floor, not a release; nothing to build"
|
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
# Nor is a prerelease, and this trigger is `v*`, which matches
|
|
# `v0.4.0-beta.1`. Two reasons it is worst here. The APK goes
|
|
# to the *generic* registry, which is readable without
|
|
# credentials so Obtainium can poll a plain URL — a beta would
|
|
# be offered to every device on it. And the versionCode maths
|
|
# below splits on dots and would read "1" out of "0-beta",
|
|
# producing a code that is wrong rather than a build that
|
|
# fails: Android orders releases by that integer and refuses
|
|
# anything not greater than what is installed.
|
|
case "$v" in
|
|
*-*)
|
|
echo "v$v is a prerelease; not publishing an APK for it"
|
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
;;
|
|
esac
|
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
|
|
# Android orders releases by an integer and refuses anything
|
|
# not greater than what is installed. 1.3.1 -> 10301, which
|
|
# increases as long as minor and patch stay below 100.
|
|
IFS=. read -r maj min pat <<EOF
|
|
$v
|
|
EOF
|
|
code=$(( ${maj:-0} * 10000 + ${min:-0} * 100 + ${pat:-0} ))
|
|
if [ "$code" -le 0 ]; then
|
|
echo "refusing to build version '$v' (versionCode $code)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "version=$v" >> "$GITHUB_OUTPUT"
|
|
echo "code=$code" >> "$GITHUB_OUTPUT"
|
|
echo "tag=v$v" >> "$GITHUB_OUTPUT"
|
|
echo "building $v (versionCode $code)"
|
|
|
|
# Releases restarted at 0.0.1 when they became automatic (plan
|
|
# 017), so versionCode restarted at 1 — *below* the 10300 an
|
|
# installed 1.3.0 build carries. Android refuses a downgrade
|
|
# outright, and the only remedy is an uninstall, which takes the
|
|
# user's library with it. Said here because this is the file
|
|
# that computes the number.
|
|
if [ "$code" -lt 10600 ]; then
|
|
echo
|
|
echo "note: versionCode $code is below the 10600 that v1.6.0 shipped."
|
|
echo " An existing install must be removed before this one will"
|
|
echo " install, and that removal takes its library with it."
|
|
fi
|
|
|
|
- name: Go toolchain
|
|
if: steps.version.outputs.skip == 'false'
|
|
run: |
|
|
set -eu
|
|
if [ ! -x /cache/tool/go/bin/go ] || ! /cache/tool/go/bin/go version | grep -q "$GO_VERSION"; then
|
|
mkdir -p /cache/tool && rm -rf /cache/tool/go
|
|
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" | tar -C /cache/tool -xz
|
|
fi
|
|
echo "/cache/tool/go/bin" >> "$GITHUB_PATH"
|
|
/cache/tool/go/bin/go version
|
|
|
|
- name: Node toolchain
|
|
if: steps.version.outputs.skip == 'false'
|
|
run: |
|
|
set -eu
|
|
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
|
apt-get install -y -qq --no-install-recommends nodejs
|
|
corepack enable
|
|
node --version
|
|
|
|
# Idempotent by directory check. sdkmanager is itself idempotent
|
|
# but still spends minutes verifying, so the guards are what make
|
|
# this cheap on every run after the first.
|
|
- name: Android SDK and NDK (cached)
|
|
if: steps.version.outputs.skip == 'false'
|
|
run: |
|
|
set -eu
|
|
mkdir -p "$ANDROID_HOME/cmdline-tools"
|
|
|
|
if [ ! -x "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" ]; then
|
|
echo "command line tools: installing"
|
|
cd /tmp
|
|
curl -fsSL -o tools.zip \
|
|
https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
|
|
unzip -q tools.zip
|
|
rm -rf "$ANDROID_HOME/cmdline-tools/latest"
|
|
mv cmdline-tools "$ANDROID_HOME/cmdline-tools/latest"
|
|
else
|
|
echo "command line tools: cached"
|
|
fi
|
|
|
|
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
|
|
yes | sdkmanager --licenses >/dev/null 2>&1 || true
|
|
|
|
install_if_missing() {
|
|
if [ -d "$ANDROID_HOME/$2" ]; then
|
|
echo "$1: cached"
|
|
else
|
|
echo "$1: installing"
|
|
yes | sdkmanager --install "$1" >/dev/null
|
|
fi
|
|
}
|
|
# android-35 matches compileSdk/targetSdk in
|
|
# build/android/app/build.gradle. No system image and no
|
|
# emulator: this job builds, it does not run.
|
|
install_if_missing "platform-tools" "platform-tools"
|
|
install_if_missing "platforms;android-35" "platforms/android-35"
|
|
install_if_missing "build-tools;34.0.0" "build-tools/34.0.0"
|
|
install_if_missing "ndk;${NDK_VERSION}" "ndk/${NDK_VERSION}"
|
|
|
|
echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/${NDK_VERSION}" >> "$GITHUB_ENV"
|
|
du -sh "$ANDROID_HOME" || true
|
|
|
|
# **Signing is not optional past the first install.** Android
|
|
# refuses to update an app whose signing key changed and the only
|
|
# remedy is an uninstall, which takes the user's library with it.
|
|
# build.gradle falls back to the *debug* keystore when these are
|
|
# absent, and that key differs between every machine and every
|
|
# runner — so publishing an unsigned build is a decision to
|
|
# reinstall by hand for ever. Fail instead.
|
|
# **Signing is not optional past the first install.** Android
|
|
# refuses to update an app whose signing key changed and the only
|
|
# remedy is an uninstall, which takes the user's library with it.
|
|
# build.gradle falls back to the *debug* keystore when these are
|
|
# absent, and that key differs between every machine and every
|
|
# runner — so publishing an unsigned build is a decision to
|
|
# reinstall by hand for ever. Fail instead.
|
|
#
|
|
# Decode, check and build are one step on purpose. Splitting them
|
|
# would mean either handing the password to a later step through
|
|
# `$GITHUB_ENV` — where the `env:` dump is only masked for values
|
|
# that are *verbatim* a secret, so a trimmed one could print in
|
|
# clear — or repeating the trimming logic in both.
|
|
- name: Build the signed APK
|
|
if: steps.version.outputs.skip == 'false'
|
|
working-directory: /src
|
|
env:
|
|
KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }}
|
|
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
|
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
|
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
|
YJ_VERSION: ${{ steps.version.outputs.version }}
|
|
YJ_VERSION_CODE: ${{ steps.version.outputs.code }}
|
|
run: |
|
|
set -eu
|
|
|
|
if [ -z "${KEYSTORE_B64:-}" ]; then
|
|
echo "ANDROID_KEYSTORE_B64 is not set."
|
|
echo
|
|
echo "Building without it signs with the debug key, and every future"
|
|
echo "update then fails with a signature mismatch. See"
|
|
echo "docs/android-release.md for the keytool command and the secrets."
|
|
exit 1
|
|
fi
|
|
if [ -z "${KEYSTORE_PASSWORD:-}" ]; then
|
|
echo "ANDROID_KEYSTORE_PASSWORD is not set — see docs/android-release.md" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# The path is decided here rather than composed in an `env:`
|
|
# block: `${{ env.HOME }}` evaluates to an empty string in
|
|
# Gitea's expression context, which turns "$HOME/x.jks" into
|
|
# "/x.jks" — reported by Gradle as a missing file, a minute in.
|
|
keystore="${RUNNER_TEMP:-/tmp}/yellowjacket-release.jks"
|
|
printf '%s' "$KEYSTORE_B64" | base64 -d > "$keystore"
|
|
chmod 600 "$keystore"
|
|
|
|
# **A secret pasted into a web form very often carries a
|
|
# trailing newline**, and a password is compared byte for byte.
|
|
# Trim CR and LF from all three, and say so when it mattered —
|
|
# "the keystore did not open" with a correct password is an
|
|
# unpleasant thing to debug blind.
|
|
pass=$(printf '%s' "$KEYSTORE_PASSWORD" | tr -d '\r\n')
|
|
if [ "${#pass}" -ne "${#KEYSTORE_PASSWORD}" ]; then
|
|
echo "note: stripped newline(s) from ANDROID_KEYSTORE_PASSWORD"
|
|
fi
|
|
alias_want=$(printf '%s' "${KEY_ALIAS:-yellowjacket}" | tr -d '\r\n')
|
|
keypass=$(printf '%s' "${KEY_PASSWORD:-$pass}" | tr -d '\r\n')
|
|
|
|
# Describe the artifact before trying to open it. A truncated
|
|
# or mis-pasted base64 yields a file that is the wrong size or
|
|
# has no keystore header at all, and that is a different
|
|
# problem from a wrong password.
|
|
size=$(stat -c %s "$keystore")
|
|
magic=$(od -An -N4 -tx1 "$keystore" | tr -s ' ' | sed 's/^ //')
|
|
echo "keystore: $size bytes, first four bytes: $magic"
|
|
|
|
# The fingerprint of the decoded file, so "is the secret the
|
|
# keystore I have locally?" is answerable without guessing.
|
|
# A hash of a *public* certificate store gives nothing away,
|
|
# and the alternative is comparing byte counts by eye.
|
|
#
|
|
# sha256sum ~/path/to/yellowjacket-release.jks
|
|
#
|
|
# A password that is right for one keystore and wrong for
|
|
# another is indistinguishable from a wrong password, and this
|
|
# is the line that distinguishes them.
|
|
echo " sha256: $(sha256sum "$keystore" | cut -d' ' -f1)"
|
|
case "$magic" in
|
|
"30 82"*) echo " header: PKCS12 (keytool's default since JDK 9)" ;;
|
|
"fe ed fe ed") echo " header: legacy JKS" ;;
|
|
*) echo " WARNING: not a keystore header. Is the secret the base64 of the .jks?" ;;
|
|
esac
|
|
|
|
# Open it here rather than letting Gradle discover the problem
|
|
# at :app:validateSigningRelease, a minute of build time in and
|
|
# reported as a missing file rather than a bad password.
|
|
if ! keytool -list -keystore "$keystore" -storepass "$pass" >/tmp/ks.txt 2>/tmp/ks.err; then
|
|
echo "the keystore did not open with ANDROID_KEYSTORE_PASSWORD." >&2
|
|
echo " password length after trimming: ${#pass}" >&2
|
|
sed 's/^/ keytool: /' /tmp/ks.err | head -5 >&2
|
|
echo >&2
|
|
|
|
# A password pasted *with its shell quotes* is the one
|
|
# remaining cause that looks identical to a wrong password:
|
|
# the secret is two characters longer than the password and
|
|
# nothing in the error says so. Naming it is safe --
|
|
# stripping the quotes and carrying on would not be, since a
|
|
# password may legitimately contain them.
|
|
unquoted=$(printf '%s' "$pass" | sed "s/^['\"]//;s/['\"]$//")
|
|
if [ "$unquoted" != "$pass" ] &&
|
|
keytool -list -keystore "$keystore" -storepass "$unquoted" >/dev/null 2>&1; then
|
|
echo " ** it opens with the surrounding quotes removed. **" >&2
|
|
echo " Re-paste ANDROID_KEYSTORE_PASSWORD without them." >&2
|
|
echo >&2
|
|
fi
|
|
echo "Check it locally with the same two values:" >&2
|
|
echo " printf %s \"\$SECRET_B64\" | base64 -d > /tmp/k.jks" >&2
|
|
echo " keytool -list -keystore /tmp/k.jks -storepass '<password>'" >&2
|
|
exit 1
|
|
fi
|
|
echo "keystore opens with the supplied password"
|
|
|
|
# And check the alias now, for the same reason. It defaults to
|
|
# `yellowjacket`, so a keystore created with any other alias
|
|
# would otherwise fail deep inside Gradle.
|
|
if ! keytool -list -keystore "$keystore" -storepass "$pass" -alias "$alias_want" >/dev/null 2>&1; then
|
|
echo "alias '$alias_want' is not in this keystore. It holds:" >&2
|
|
sed -n 's/^\([^,]*\),.*Entry.*$/ \1/p' /tmp/ks.txt >&2
|
|
echo "Set ANDROID_KEY_ALIAS to one of those." >&2
|
|
exit 1
|
|
fi
|
|
echo "alias '$alias_want': present"
|
|
|
|
ANDROID_KEYSTORE_FILE="$keystore"
|
|
ANDROID_KEYSTORE_PASSWORD="$pass"
|
|
ANDROID_KEY_ALIAS="$alias_want"
|
|
ANDROID_KEY_PASSWORD="$keypass"
|
|
export ANDROID_KEYSTORE_FILE ANDROID_KEYSTORE_PASSWORD
|
|
export ANDROID_KEY_ALIAS ANDROID_KEY_PASSWORD
|
|
|
|
# ANDROID_SDK is passed explicitly: the Makefile defaults it to
|
|
# ~/Android/Sdk, which is the developer-machine layout and not
|
|
# this container's.
|
|
make android ANDROID_SDK="$ANDROID_HOME" ANDROID_NDK="$ANDROID_NDK_HOME"
|
|
|
|
- name: Verify the APK
|
|
id: apk
|
|
if: steps.version.outputs.skip == 'false'
|
|
working-directory: /src
|
|
run: |
|
|
set -eu
|
|
apk=bin/yellowjacket.apk
|
|
[ -s "$apk" ] || { echo "no APK was produced" >&2; ls -la bin || true; exit 1; }
|
|
bt="$ANDROID_HOME/build-tools/34.0.0"
|
|
|
|
ls -la "$apk"
|
|
"$bt/aapt2" dump badging "$apk" | sed -n '1p;/application-label:/p;/native-code/p'
|
|
|
|
# arm64 and *only* arm64. x86_64 Android cannot run this app
|
|
# (modernc's raw lstat against Android's seccomp filter, which
|
|
# is every x86_64 device and not merely the emulator), so an
|
|
# x86_64 slice would be ~31 MB that runs nowhere -- and its
|
|
# reappearance would mean someone had put the ABI back in
|
|
# app/build.gradle without knowing that.
|
|
"$bt/aapt2" dump badging "$apk" | grep -q "native-code: 'arm64-v8a'$" || {
|
|
echo "the APK's ABI set is not exactly arm64-v8a" >&2; exit 1; }
|
|
|
|
# The identity the pipeline exists to keep stable.
|
|
"$bt/aapt2" dump badging "$apk" | grep -q "versionCode='${{ steps.version.outputs.code }}'" || {
|
|
echo "versionCode is not ${{ steps.version.outputs.code }}" >&2; exit 1; }
|
|
|
|
echo
|
|
"$bt/apksigner" verify --print-certs "$apk" |
|
|
grep -E 'Signer #1 certificate (DN|SHA-256 digest)'
|
|
|
|
# A build signed with the debug key installs once and can never
|
|
# be updated. It must never reach the registry.
|
|
if "$bt/apksigner" verify --print-certs "$apk" | grep -q 'CN=Android Debug'; then
|
|
echo "REFUSING TO PUBLISH: signed with the debug keystore" >&2
|
|
exit 1
|
|
fi
|
|
echo
|
|
echo "Record that SHA-256. If it ever changes, updates will fail."
|
|
|
|
# Two copies: a versioned one for history and a fixed `latest` URL
|
|
# for Obtainium to watch. Gitea refuses to overwrite an existing
|
|
# file, so `latest` is deleted first. Credentials are the same
|
|
# OWNER/PACKAGE_TOKEN pair arch-package.yml publishes with.
|
|
- name: Publish to the Gitea package registry
|
|
if: steps.version.outputs.skip == 'false'
|
|
working-directory: /src
|
|
env:
|
|
VERSION: ${{ steps.version.outputs.version }}
|
|
run: |
|
|
set -eu
|
|
base="${SERVER_URL}/api/packages/${OWNER}/generic/${PACKAGE_NAME}"
|
|
apk=bin/yellowjacket.apk
|
|
|
|
put() {
|
|
code=$(curl -s -o /tmp/put.out -w '%{http_code}' \
|
|
--user "${OWNER}:${PACKAGE_TOKEN}" \
|
|
--upload-file "$apk" "$1")
|
|
echo " -> $1 : $code"
|
|
# 409 is "already there", which is the correct outcome for a
|
|
# re-run of the same tag and not a failure.
|
|
if [ "$code" != "201" ] && [ "$code" != "409" ]; then
|
|
cat /tmp/put.out >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
echo "publishing the versioned copy"
|
|
put "$base/$VERSION/yellowjacket-$VERSION.apk"
|
|
|
|
echo "clearing the previous latest"
|
|
curl -s -o /dev/null -w ' -> delete latest: %{http_code}\n' \
|
|
--user "${OWNER}:${PACKAGE_TOKEN}" \
|
|
-X DELETE "$base/latest/yellowjacket.apk" || true
|
|
|
|
echo "publishing latest"
|
|
put "$base/latest/yellowjacket.apk"
|
|
|
|
echo
|
|
echo "Obtainium URL:"
|
|
echo " $base/latest/yellowjacket.apk"
|
|
|
|
# The generic registry is what Obtainium polls; the release page is
|
|
# what a person looks at. Same file, already built and already
|
|
# verified by the step above — so this cannot publish something the
|
|
# signature check would have refused.
|
|
- name: Attach the APK to the release
|
|
if: steps.version.outputs.skip == 'false'
|
|
working-directory: /src
|
|
env:
|
|
TAG: ${{ steps.version.outputs.tag }}
|
|
VERSION: ${{ steps.version.outputs.version }}
|
|
run: |
|
|
set -eu
|
|
./scripts/release-asset.sh "$TAG" bin/yellowjacket.apk \
|
|
"yellowjacket-${VERSION}-android-arm64.apk"
|