mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
ci: add Android release workflow
This commit is contained in:
parent
3fd4b2bfc2
commit
15ea5d857f
3 changed files with 382 additions and 0 deletions
192
.github/workflows/android-release.yml
vendored
Normal file
192
.github/workflows/android-release.yml
vendored
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
name: Build Android Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
mode:
|
||||
description: Build only, create a draft, or publish the release
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- dry-run
|
||||
- draft
|
||||
- publish
|
||||
default: dry-run
|
||||
exclude_commits:
|
||||
description: Optional comma-separated commit hashes to omit from the notes
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: android-release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: ${{ inputs.mode }} Android release
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_PROPERTIES_BASE64: ${{ secrets.NUVIO_LOCAL_PROPERTIES_BASE64 }}
|
||||
RELEASE_KEYSTORE_BASE64: ${{ secrets.NUVIO_RELEASE_KEYSTORE_BASE64 }}
|
||||
|
||||
steps:
|
||||
- name: Check out full history
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Read release metadata
|
||||
id: release
|
||||
run: ./scripts/release-metadata.sh "${GITHUB_SHA}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Validate release state
|
||||
env:
|
||||
RELEASE_TAG: ${{ steps.release.outputs.tag }}
|
||||
CURRENT_BUMP: ${{ steps.release.outputs.current_bump }}
|
||||
RELEASE_COMMIT: ${{ steps.release.outputs.release_commit }}
|
||||
CURRENT_BRANCH: ${{ github.ref_name }}
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
run: |
|
||||
if [[ "${CURRENT_BRANCH}" != "${DEFAULT_BRANCH}" ]]; then
|
||||
echo "Releases must run from the default branch (${DEFAULT_BRANCH}), not ${CURRENT_BRANCH}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if git rev-parse --verify --quiet "refs/tags/${RELEASE_TAG}"; then
|
||||
echo "Tag ${RELEASE_TAG} already exists." >&2
|
||||
exit 1
|
||||
fi
|
||||
if gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}" >/dev/null 2>&1; then
|
||||
echo "A GitHub release for ${RELEASE_TAG} already exists." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
unexpected_changes="$(
|
||||
git diff --name-only "${CURRENT_BUMP}..${RELEASE_COMMIT}" \
|
||||
| grep -Ev '^(\.github/workflows/android-release\.yml|scripts/(generate-release-notes|release-metadata)\.sh)$' \
|
||||
|| true
|
||||
)"
|
||||
if [[ -n "${unexpected_changes}" ]]; then
|
||||
echo "The version bump must be the final application change before release." >&2
|
||||
echo "Unexpected files changed after the bump:" >&2
|
||||
echo "${unexpected_changes}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
required_secrets=(
|
||||
RELEASE_PROPERTIES_BASE64
|
||||
RELEASE_KEYSTORE_BASE64
|
||||
)
|
||||
missing=()
|
||||
for secret_name in "${required_secrets[@]}"; do
|
||||
if [[ -z "${!secret_name}" ]]; then
|
||||
missing+=("${secret_name}")
|
||||
fi
|
||||
done
|
||||
if (( ${#missing[@]} > 0 )); then
|
||||
printf 'Missing required release secrets: %s\n' "${missing[*]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Generate release notes
|
||||
env:
|
||||
PREVIOUS_BUMP: ${{ steps.release.outputs.previous_bump }}
|
||||
CURRENT_BUMP: ${{ steps.release.outputs.current_bump }}
|
||||
EXCLUDE_COMMITS: ${{ inputs.exclude_commits }}
|
||||
run: |
|
||||
./scripts/generate-release-notes.sh \
|
||||
--from "${PREVIOUS_BUMP}" \
|
||||
--to "${CURRENT_BUMP}" \
|
||||
--repository "${GITHUB_REPOSITORY}" \
|
||||
--exclude "${EXCLUDE_COMMITS}" \
|
||||
> release-notes.md
|
||||
|
||||
if [[ ! -s release-notes.md ]]; then
|
||||
echo "No release-note commits were found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
{
|
||||
echo "## ${{ steps.release.outputs.version }} release notes"
|
||||
echo
|
||||
cat release-notes.md
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
|
||||
- name: Configure release properties
|
||||
env:
|
||||
RELEASE_MODE: ${{ inputs.mode }}
|
||||
run: |
|
||||
keystore_path="${RUNNER_TEMP}/nuvio-release.jks"
|
||||
printf '%s' "${RELEASE_KEYSTORE_BASE64}" | base64 --decode > "${keystore_path}"
|
||||
printf '%s' "${RELEASE_PROPERTIES_BASE64}" | base64 --decode > local.properties
|
||||
|
||||
# These paths only apply to the machine where local.properties was encoded.
|
||||
sed -i -E '/^[[:space:]]*(sdk\.dir|NUVIO_RELEASE_STORE_FILE)[[:space:]]*=/d' local.properties
|
||||
printf '\nNUVIO_RELEASE_STORE_FILE=%s\n' "${keystore_path}" >> local.properties
|
||||
|
||||
# A dry run must not upload ProGuard mappings or mutate release services.
|
||||
if [[ "${RELEASE_MODE}" == "dry-run" ]]; then
|
||||
sed -i -E '/^[[:space:]]*SENTRY_(AUTH_TOKEN|ORG|PROJECT)[[:space:]]*=/d' local.properties
|
||||
fi
|
||||
|
||||
required_properties=(
|
||||
NUVIO_RELEASE_STORE_PASSWORD
|
||||
NUVIO_RELEASE_KEY_ALIAS
|
||||
NUVIO_RELEASE_KEY_PASSWORD
|
||||
TRAKT_CLIENT_ID
|
||||
TRAKT_CLIENT_SECRET
|
||||
)
|
||||
for property_name in "${required_properties[@]}"; do
|
||||
if ! grep -Eq "^${property_name}=.+" local.properties; then
|
||||
echo "Missing required property: ${property_name}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Build full release APKs
|
||||
run: ./gradlew :androidApp:assembleFullRelease --stacktrace
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: nuvio-${{ steps.release.outputs.version }}-full-release
|
||||
path: androidApp/build/outputs/apk/full/release/*.apk
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create GitHub release
|
||||
if: inputs.mode != 'dry-run'
|
||||
env:
|
||||
RELEASE_MODE: ${{ inputs.mode }}
|
||||
RELEASE_TAG: ${{ steps.release.outputs.tag }}
|
||||
RELEASE_VERSION: ${{ steps.release.outputs.version }}
|
||||
RELEASE_COMMIT: ${{ steps.release.outputs.release_commit }}
|
||||
run: |
|
||||
mapfile -t release_apks < <(find androidApp/build/outputs/apk/full/release -maxdepth 1 -type f -name '*.apk' -print | sort)
|
||||
if (( ${#release_apks[@]} == 0 )); then
|
||||
echo "No release APKs were produced." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
release_args=(
|
||||
"${RELEASE_TAG}"
|
||||
--target "${RELEASE_COMMIT}"
|
||||
--title "${RELEASE_VERSION}"
|
||||
--notes-file release-notes.md
|
||||
)
|
||||
if [[ "${RELEASE_MODE}" == "draft" ]]; then
|
||||
release_args+=(--draft)
|
||||
fi
|
||||
gh release create "${release_args[@]}" "${release_apks[@]}"
|
||||
136
scripts/generate-release-notes.sh
Executable file
136
scripts/generate-release-notes.sh
Executable file
|
|
@ -0,0 +1,136 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
from_ref=""
|
||||
to_ref="HEAD"
|
||||
repository="${GITHUB_REPOSITORY:-}"
|
||||
offline=false
|
||||
exclude_commits="${RELEASE_NOTES_EXCLUDE_COMMITS:-}"
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 --from <commit> [--to <commit>] [--repository <owner/repo>] [--exclude <hashes>] [--offline]" >&2
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--from)
|
||||
from_ref="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--to)
|
||||
to_ref="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--repository)
|
||||
repository="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--exclude)
|
||||
exclude_commits="${exclude_commits} ${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--offline)
|
||||
offline=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$from_ref" ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git cat-file -e "${from_ref}^{commit}" 2>/dev/null || {
|
||||
echo "Unknown starting commit: ${from_ref}" >&2
|
||||
exit 1
|
||||
}
|
||||
git cat-file -e "${to_ref}^{commit}" 2>/dev/null || {
|
||||
echo "Unknown ending commit: ${to_ref}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
is_excluded_hash() {
|
||||
local commit="$1"
|
||||
local excluded
|
||||
for excluded in ${exclude_commits//,/ }; do
|
||||
[[ -n "$excluded" ]] || continue
|
||||
if [[ "$commit" == "$excluded"* ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
is_release_note() {
|
||||
local subject_lower
|
||||
local version_bump_pattern='^(bump([[:space:]].*)?version|version[[:space:]]+bump)([[:space:]].*)?$'
|
||||
local cleanup_pattern='^cleanup([[:space:][:punct:]].*)?$'
|
||||
local conventional_noise_pattern='^(build|chore|ci|docs|style|test)(\([^)]*\))?:'
|
||||
subject_lower="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
[[ "$subject_lower" != *"[skip release notes]"* ]] || return 1
|
||||
[[ ! "$subject_lower" =~ $version_bump_pattern ]] || return 1
|
||||
[[ ! "$subject_lower" =~ $cleanup_pattern ]] || return 1
|
||||
[[ ! "$subject_lower" =~ $conventional_noise_pattern ]] || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
resolve_username() {
|
||||
local commit="$1"
|
||||
local author_name="$2"
|
||||
local author_email="$3"
|
||||
local username=""
|
||||
|
||||
if [[ "$author_email" =~ ^[0-9]+\+([^@]+)@users\.noreply\.github\.com$ ]]; then
|
||||
username="${BASH_REMATCH[1]}"
|
||||
elif [[ "$author_email" =~ ^([^@]+)@users\.noreply\.github\.com$ ]]; then
|
||||
username="${BASH_REMATCH[1]}"
|
||||
elif [[ "$offline" == false && -n "$repository" && -n "${GH_TOKEN:-}" ]] && command -v gh >/dev/null 2>&1; then
|
||||
username="$(gh api "repos/${repository}/commits/${commit}" --jq '.author.login // empty' 2>/dev/null || true)"
|
||||
if [[ -z "$username" ]]; then
|
||||
username="$(
|
||||
gh api \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
"repos/${repository}/commits/${commit}/pulls" \
|
||||
--jq '.[0].user.login // empty' \
|
||||
2>/dev/null \
|
||||
|| true
|
||||
)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$username" ]]; then
|
||||
username="$(printf '%s' "$author_name" | tr -cd '[:alnum:]_-')"
|
||||
fi
|
||||
printf '%s' "${username:-unknown}"
|
||||
}
|
||||
|
||||
seen_subjects=$'\n'
|
||||
separator=$'\x1f'
|
||||
|
||||
while IFS="$separator" read -r commit short_hash subject author_name author_email; do
|
||||
[[ -n "$commit" ]] || continue
|
||||
is_excluded_hash "$commit" && continue
|
||||
is_release_note "$subject" || continue
|
||||
|
||||
normalized_subject="$(printf '%s' "$subject" | tr '[:upper:]' '[:lower:]' | sed -E 's/[[:space:]]+/ /g; s/[[:space:].]+$//')"
|
||||
[[ "$seen_subjects" != *$'\n'"$normalized_subject"$'\n'* ]] || continue
|
||||
seen_subjects+="${normalized_subject}"$'\n'
|
||||
|
||||
display_subject="$(printf '%s' "$subject" | sed -E 's/[[:space:]]+$//; s/\.$//')"
|
||||
username="$(resolve_username "$commit" "$author_name" "$author_email")"
|
||||
printf '%s %s @%s \n' "$short_hash" "$display_subject" "$username"
|
||||
done < <(
|
||||
git log "${from_ref}..${to_ref}" --no-merges \
|
||||
--format="%H${separator}%h${separator}%s${separator}%an${separator}%ae"
|
||||
)
|
||||
54
scripts/release-metadata.sh
Executable file
54
scripts/release-metadata.sh
Executable file
|
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
version_file="${VERSION_FILE:-iosApp/Configuration/Version.xcconfig}"
|
||||
target_ref="${1:-HEAD}"
|
||||
|
||||
if ! git cat-file -e "${target_ref}^{commit}" 2>/dev/null; then
|
||||
echo "Unknown release target: ${target_ref}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
read_version() {
|
||||
local commit="$1"
|
||||
git show "${commit}:${version_file}" \
|
||||
| sed -nE 's/^[[:space:]]*MARKETING_VERSION[[:space:]]*=[[:space:]]*([^[:space:]#]+).*$/\1/p' \
|
||||
| head -n 1
|
||||
}
|
||||
|
||||
current_version=""
|
||||
current_bump=""
|
||||
previous_version=""
|
||||
previous_bump=""
|
||||
|
||||
while IFS= read -r commit; do
|
||||
version="$(read_version "$commit")"
|
||||
[[ -n "$version" ]] || continue
|
||||
|
||||
if [[ -z "$current_version" ]]; then
|
||||
current_version="$version"
|
||||
current_bump="$commit"
|
||||
elif [[ "$version" != "$current_version" ]]; then
|
||||
previous_version="$version"
|
||||
previous_bump="$commit"
|
||||
break
|
||||
fi
|
||||
done < <(git log "$target_ref" --format='%H' -- "$version_file")
|
||||
|
||||
if [[ -z "$current_bump" || -z "$previous_bump" ]]; then
|
||||
echo "Could not find two distinct version bumps in ${version_file}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! "$current_version" =~ ^[0-9A-Za-z][0-9A-Za-z._-]*$ ]]; then
|
||||
echo "Invalid release version: ${current_version}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'version=%s\n' "$current_version"
|
||||
printf 'tag=%s\n' "$current_version"
|
||||
printf 'release_commit=%s\n' "$(git rev-parse "${target_ref}^{commit}")"
|
||||
printf 'current_bump=%s\n' "$current_bump"
|
||||
printf 'previous_version=%s\n' "$previous_version"
|
||||
printf 'previous_bump=%s\n' "$previous_bump"
|
||||
Loading…
Reference in a new issue