The release workflow now derives versionName and versionCode from the tag and validates the tag format. It updates multiple project files in the runner workspace without committing changes back to the repository. A verification step ensures the generated Android properties are correct. - Tag parsing validates vMAJOR.MINOR.PATCH and enforces minor/patch <= 999 - VersionCode computed as major*1e6 + minor*1e3 + patch and applied - Added post-build verification of Android versionName and versionCode
267 lines
11 KiB
YAML
267 lines
11 KiB
YAML
name: Android Release
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- "v*"
|
|
workflow_dispatch:
|
|
|
|
jobs:
|
|
build-release-apk:
|
|
name: Build signed release APK
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 60
|
|
permissions:
|
|
code: read
|
|
releases: write
|
|
env:
|
|
ANDROID_COMPILE_SDK: "36"
|
|
ANDROID_BUILD_TOOLS: "36.0.0"
|
|
ANDROID_NDK_VERSION: "30.0.15729638"
|
|
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
|
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
|
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
|
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
|
GITEA_RELEASE_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
|
|
steps:
|
|
- name: Check out repository
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set up Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "22"
|
|
cache: npm
|
|
|
|
- name: Set up Java
|
|
uses: actions/setup-java@v4
|
|
with:
|
|
distribution: temurin
|
|
java-version: "21"
|
|
cache: gradle
|
|
|
|
- name: Set up Rust
|
|
uses: dtolnay/rust-toolchain@1.95.0
|
|
with:
|
|
targets: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android
|
|
|
|
- name: Cache Rust build
|
|
uses: Swatinem/rust-cache@v2
|
|
with:
|
|
workspaces: src-tauri -> target
|
|
|
|
- name: Set up Android SDK
|
|
uses: android-actions/setup-android@v3
|
|
|
|
- name: Install Android SDK packages
|
|
run: |
|
|
yes | sdkmanager --licenses >/dev/null || true
|
|
sdkmanager \
|
|
"platforms;android-${ANDROID_COMPILE_SDK}" \
|
|
"build-tools;${ANDROID_BUILD_TOOLS}" \
|
|
"ndk;${ANDROID_NDK_VERSION}"
|
|
echo "NDK_HOME=${ANDROID_HOME}/ndk/${ANDROID_NDK_VERSION}" >> "${GITEA_ENV}"
|
|
echo "${ANDROID_HOME}/build-tools/${ANDROID_BUILD_TOOLS}" >> "${GITEA_PATH}"
|
|
|
|
- name: Install JavaScript dependencies
|
|
run: npm ci
|
|
|
|
- name: Apply version from release tag
|
|
run: |
|
|
if [ "${GITHUB_REF_TYPE}" != "tag" ]; then
|
|
echo "Release builds must run on a tag such as v0.1.0." >&2
|
|
exit 1
|
|
fi
|
|
|
|
RELEASE_VERSION="${GITHUB_REF_NAME#v}"
|
|
if [[ ! "${RELEASE_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
echo "Tag ${GITHUB_REF_NAME} must use the format vMAJOR.MINOR.PATCH." >&2
|
|
exit 1
|
|
fi
|
|
|
|
IFS=. read -r VERSION_MAJOR VERSION_MINOR VERSION_PATCH <<< "${RELEASE_VERSION}"
|
|
if (( 10#${VERSION_MINOR} > 999 || 10#${VERSION_PATCH} > 999 )); then
|
|
echo "Minor and patch versions must not exceed 999." >&2
|
|
exit 1
|
|
fi
|
|
ANDROID_VERSION_CODE=$((10#${VERSION_MAJOR} * 1000000 + 10#${VERSION_MINOR} * 1000 + 10#${VERSION_PATCH}))
|
|
if (( ANDROID_VERSION_CODE < 1 || ANDROID_VERSION_CODE > 2100000000 )); then
|
|
echo "Derived Android versionCode ${ANDROID_VERSION_CODE} is outside the allowed range." >&2
|
|
exit 1
|
|
fi
|
|
|
|
RELEASE_VERSION="${RELEASE_VERSION}" \
|
|
ANDROID_VERSION_CODE="${ANDROID_VERSION_CODE}" \
|
|
GITHUB_REF_NAME="${GITHUB_REF_NAME}" \
|
|
node -e '
|
|
const fs = require("fs");
|
|
const version = process.env.RELEASE_VERSION;
|
|
const versionCode = process.env.ANDROID_VERSION_CODE;
|
|
|
|
function updateJson(path, update) {
|
|
const value = JSON.parse(fs.readFileSync(path, "utf8"));
|
|
update(value);
|
|
fs.writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
}
|
|
|
|
updateJson("package.json", value => { value.version = version; });
|
|
updateJson("package-lock.json", value => {
|
|
value.version = version;
|
|
value.packages[""].version = version;
|
|
});
|
|
updateJson("src-tauri/tauri.conf.json", value => { value.version = version; });
|
|
|
|
const cargoTomlPath = "src-tauri/Cargo.toml";
|
|
const cargoToml = fs.readFileSync(cargoTomlPath, "utf8").replace(
|
|
/(\[package\][\s\S]*?\nversion = ")[^"]+("\n)/,
|
|
`$1${version}$2`
|
|
);
|
|
fs.writeFileSync(cargoTomlPath, cargoToml);
|
|
|
|
const cargoLockPath = "src-tauri/Cargo.lock";
|
|
const cargoLock = fs.readFileSync(cargoLockPath, "utf8").replace(
|
|
/(\[\[package\]\]\nname = "lockscreenwallpaper"\nversion = ")[^"]+("\n)/,
|
|
`$1${version}$2`
|
|
);
|
|
fs.writeFileSync(cargoLockPath, cargoLock);
|
|
|
|
const fdroidPath = ".fdroid.yml";
|
|
let fdroid = fs.readFileSync(fdroidPath, "utf8");
|
|
fdroid = fdroid
|
|
.replace(/^ - versionName: .*$/m, ` - versionName: ${version}`)
|
|
.replace(/^ versionCode: .*$/m, ` versionCode: ${versionCode}`)
|
|
.replace(/^ commit: .*$/m, ` commit: ${process.env.GITHUB_REF_NAME}`)
|
|
.replace(/^CurrentVersion: .*$/m, `CurrentVersion: ${version}`)
|
|
.replace(/^CurrentVersionCode: .*$/m, `CurrentVersionCode: ${versionCode}`);
|
|
fs.writeFileSync(fdroidPath, fdroid);
|
|
'
|
|
|
|
echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "${GITEA_ENV}"
|
|
echo "ANDROID_VERSION_CODE=${ANDROID_VERSION_CODE}" >> "${GITEA_ENV}"
|
|
echo "Building ${GITHUB_REF_NAME} with Android versionCode ${ANDROID_VERSION_CODE}."
|
|
|
|
- name: Validate signing secrets
|
|
run: |
|
|
for SECRET_NAME in \
|
|
ANDROID_KEYSTORE_BASE64 \
|
|
ANDROID_KEYSTORE_PASSWORD \
|
|
ANDROID_KEY_ALIAS \
|
|
ANDROID_KEY_PASSWORD \
|
|
GITEA_RELEASE_TOKEN
|
|
do
|
|
if [ -z "${!SECRET_NAME:-}" ]; then
|
|
echo "Missing Gitea Actions secret: ${SECRET_NAME}" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
- name: Restore release keystore
|
|
run: |
|
|
SIGNING_DIRECTORY="${RUNNER_TEMP}/wallpaperflow-signing"
|
|
mkdir -p "${SIGNING_DIRECTORY}"
|
|
printf '%s' "${ANDROID_KEYSTORE_BASE64}" | base64 --decode > "${SIGNING_DIRECTORY}/release.jks"
|
|
chmod 600 "${SIGNING_DIRECTORY}/release.jks"
|
|
echo "ANDROID_SIGNING_KEYSTORE=${SIGNING_DIRECTORY}/release.jks" >> "${GITEA_ENV}"
|
|
|
|
- name: Build unsigned release APK
|
|
run: npm run tauri -- android build --apk --ci
|
|
|
|
- name: Verify generated Android version
|
|
run: |
|
|
TAURI_PROPERTIES="src-tauri/gen/android/app/tauri.properties"
|
|
grep -Fx "tauri.android.versionName=${RELEASE_VERSION}" "${TAURI_PROPERTIES}"
|
|
grep -Fx "tauri.android.versionCode=${ANDROID_VERSION_CODE}" "${TAURI_PROPERTIES}"
|
|
|
|
- name: Align and sign APK
|
|
run: |
|
|
UNSIGNED_APK="src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release-unsigned.apk"
|
|
RELEASE_DIRECTORY="release"
|
|
ALIGNED_APK="${RELEASE_DIRECTORY}/WallpaperFlow-release-aligned.apk"
|
|
SIGNED_APK="${RELEASE_DIRECTORY}/WallpaperFlow-release.apk"
|
|
|
|
test -f "${UNSIGNED_APK}"
|
|
mkdir -p "${RELEASE_DIRECTORY}"
|
|
zipalign -p -f 4 "${UNSIGNED_APK}" "${ALIGNED_APK}"
|
|
apksigner sign \
|
|
--ks "${ANDROID_SIGNING_KEYSTORE}" \
|
|
--ks-key-alias "${ANDROID_KEY_ALIAS}" \
|
|
--ks-pass env:ANDROID_KEYSTORE_PASSWORD \
|
|
--key-pass env:ANDROID_KEY_PASSWORD \
|
|
--out "${SIGNED_APK}" \
|
|
"${ALIGNED_APK}"
|
|
apksigner verify --verbose --print-certs "${SIGNED_APK}"
|
|
rm "${ALIGNED_APK}"
|
|
(cd "${RELEASE_DIRECTORY}" && sha256sum WallpaperFlow-release.apk > WallpaperFlow-release.apk.sha256)
|
|
|
|
- name: Attach APK to Gitea release
|
|
run: |
|
|
API_ROOT="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}"
|
|
RELEASE_JSON="${RUNNER_TEMP}/wallpaperflow-release.json"
|
|
CREATE_JSON="${RUNNER_TEMP}/wallpaperflow-create-release.json"
|
|
ASSETS_JSON="${RUNNER_TEMP}/wallpaperflow-release-assets.json"
|
|
|
|
STATUS="$(curl --silent --show-error \
|
|
--output "${RELEASE_JSON}" \
|
|
--write-out '%{http_code}' \
|
|
--header "Authorization: token ${GITEA_RELEASE_TOKEN}" \
|
|
"${API_ROOT}/releases/tags/${GITHUB_REF_NAME}")"
|
|
|
|
if [ "${STATUS}" = "404" ]; then
|
|
node -e '
|
|
process.stdout.write(JSON.stringify({
|
|
tag_name: process.env.GITHUB_REF_NAME,
|
|
name: `WallpaperFlow ${process.env.GITHUB_REF_NAME}`,
|
|
body: "Automatisch erstelltes Android-Release.",
|
|
draft: false,
|
|
prerelease: false
|
|
}))
|
|
' > "${CREATE_JSON}"
|
|
curl --fail --silent --show-error \
|
|
--request POST \
|
|
--header "Authorization: token ${GITEA_RELEASE_TOKEN}" \
|
|
--header "Content-Type: application/json" \
|
|
--data-binary "@${CREATE_JSON}" \
|
|
--output "${RELEASE_JSON}" \
|
|
"${API_ROOT}/releases"
|
|
elif [ "${STATUS}" != "200" ]; then
|
|
echo "Could not read Gitea release for ${GITHUB_REF_NAME} (HTTP ${STATUS})." >&2
|
|
exit 1
|
|
fi
|
|
|
|
RELEASE_ID="$(node -e '
|
|
const release = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
|
|
if (!release.id) process.exit(1);
|
|
process.stdout.write(String(release.id));
|
|
' "${RELEASE_JSON}")"
|
|
|
|
curl --fail --silent --show-error \
|
|
--header "Authorization: token ${GITEA_RELEASE_TOKEN}" \
|
|
--output "${ASSETS_JSON}" \
|
|
"${API_ROOT}/releases/${RELEASE_ID}/assets"
|
|
|
|
for FILE in \
|
|
release/WallpaperFlow-release.apk \
|
|
release/WallpaperFlow-release.apk.sha256
|
|
do
|
|
ASSET_NAME="$(basename "${FILE}")"
|
|
ASSET_ID="$(ASSET_NAME="${ASSET_NAME}" node -e '
|
|
const assets = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
|
|
const asset = assets.find(item => item.name === process.env.ASSET_NAME);
|
|
if (asset) process.stdout.write(String(asset.id));
|
|
' "${ASSETS_JSON}")"
|
|
|
|
if [ -n "${ASSET_ID}" ]; then
|
|
curl --fail --silent --show-error \
|
|
--request DELETE \
|
|
--header "Authorization: token ${GITEA_RELEASE_TOKEN}" \
|
|
"${API_ROOT}/releases/${RELEASE_ID}/assets/${ASSET_ID}"
|
|
fi
|
|
|
|
curl --fail --silent --show-error \
|
|
--request POST \
|
|
--header "Authorization: token ${GITEA_RELEASE_TOKEN}" \
|
|
--form "attachment=@${FILE}" \
|
|
"${API_ROOT}/releases/${RELEASE_ID}/assets" >/dev/null
|
|
done
|