mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-16 15:01:59 +00:00
Merge d2aa358bb9 into 70d3eee9d2
This commit is contained in:
commit
9e5da8f099
7 changed files with 780 additions and 3 deletions
89
.github/actions/write-runtime-config/action.yml
vendored
Normal file
89
.github/actions/write-runtime-config/action.yml
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
name: Write runtime config to local.properties
|
||||
description: >
|
||||
Append runtime config keys (Supabase, Trakt, IntroDB, IMDB, Community URLs)
|
||||
consumed by composeApp/build.gradle.kts to local.properties. Each input is
|
||||
optional — when omitted the corresponding generated config field is an empty
|
||||
string at runtime (matches the behavior of a local dev without a configured
|
||||
local.properties).
|
||||
|
||||
inputs:
|
||||
supabase-url:
|
||||
description: Supabase project URL
|
||||
required: false
|
||||
default: ''
|
||||
supabase-anon-key:
|
||||
description: Supabase anon API key
|
||||
required: false
|
||||
default: ''
|
||||
trakt-client-id:
|
||||
description: Trakt OAuth client id
|
||||
required: false
|
||||
default: ''
|
||||
trakt-client-secret:
|
||||
description: Trakt OAuth client secret
|
||||
required: false
|
||||
default: ''
|
||||
trakt-redirect-uri:
|
||||
description: Trakt OAuth redirect URI (defaults to nuvio://auth/trakt)
|
||||
required: false
|
||||
default: ''
|
||||
introdb-api-url:
|
||||
description: IntroDB API base URL
|
||||
required: false
|
||||
default: ''
|
||||
imdb-ratings-api-base-url:
|
||||
description: IMDB episode ratings API base URL
|
||||
required: false
|
||||
default: ''
|
||||
imdb-tapframe-api-base-url:
|
||||
description: IMDB tapframe API base URL
|
||||
required: false
|
||||
default: ''
|
||||
contributions-url:
|
||||
description: Community contributions URL (settings screen)
|
||||
required: false
|
||||
default: ''
|
||||
donations-base-url:
|
||||
description: Donations base URL (settings screen)
|
||||
required: false
|
||||
default: ''
|
||||
donations-donate-url:
|
||||
description: Donations donate URL (settings screen)
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Append runtime config to local.properties
|
||||
shell: bash
|
||||
env:
|
||||
SUPABASE_URL: ${{ inputs.supabase-url }}
|
||||
SUPABASE_ANON_KEY: ${{ inputs.supabase-anon-key }}
|
||||
TRAKT_CLIENT_ID: ${{ inputs.trakt-client-id }}
|
||||
TRAKT_CLIENT_SECRET: ${{ inputs.trakt-client-secret }}
|
||||
TRAKT_REDIRECT_URI: ${{ inputs.trakt-redirect-uri }}
|
||||
INTRODB_API_URL: ${{ inputs.introdb-api-url }}
|
||||
IMDB_RATINGS_API_BASE_URL: ${{ inputs.imdb-ratings-api-base-url }}
|
||||
IMDB_TAPFRAME_API_BASE_URL: ${{ inputs.imdb-tapframe-api-base-url }}
|
||||
CONTRIBUTIONS_URL: ${{ inputs.contributions-url }}
|
||||
DONATIONS_BASE_URL: ${{ inputs.donations-base-url }}
|
||||
DONATIONS_DONATE_URL: ${{ inputs.donations-donate-url }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
{
|
||||
echo ""
|
||||
printf 'SUPABASE_URL=%s\n' "${SUPABASE_URL}"
|
||||
printf 'SUPABASE_ANON_KEY=%s\n' "${SUPABASE_ANON_KEY}"
|
||||
printf 'TRAKT_CLIENT_ID=%s\n' "${TRAKT_CLIENT_ID}"
|
||||
printf 'TRAKT_CLIENT_SECRET=%s\n' "${TRAKT_CLIENT_SECRET}"
|
||||
# Empty input falls back to the same default the build script uses
|
||||
# when the property is missing.
|
||||
printf 'TRAKT_REDIRECT_URI=%s\n' "${TRAKT_REDIRECT_URI:-nuvio://auth/trakt}"
|
||||
printf 'INTRODB_API_URL=%s\n' "${INTRODB_API_URL}"
|
||||
printf 'IMDB_RATINGS_API_BASE_URL=%s\n' "${IMDB_RATINGS_API_BASE_URL}"
|
||||
printf 'IMDB_TAPFRAME_API_BASE_URL=%s\n' "${IMDB_TAPFRAME_API_BASE_URL}"
|
||||
printf 'CONTRIBUTIONS_URL=%s\n' "${CONTRIBUTIONS_URL}"
|
||||
printf 'DONATIONS_BASE_URL=%s\n' "${DONATIONS_BASE_URL}"
|
||||
printf 'DONATIONS_DONATE_URL=%s\n' "${DONATIONS_DONATE_URL}"
|
||||
} >> local.properties
|
||||
99
.github/workflows/build-check.yml
vendored
Normal file
99
.github/workflows/build-check.yml
vendored
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
name: Build check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
# Cancel superseded runs on the same PR.
|
||||
concurrency:
|
||||
group: build-check-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
android:
|
||||
name: Android (compile)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Make gradlew executable
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
- name: Compile Android (full + playstore, debug)
|
||||
run: ./gradlew :composeApp:assembleFullDebug :composeApp:assemblePlaystoreDebug --stacktrace
|
||||
|
||||
ios:
|
||||
name: iOS (compile)
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
NUVIO_IOS_DISTRIBUTION: full
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Select latest stable Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: latest-stable
|
||||
|
||||
- name: Make gradlew executable
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
# Both iOS distributions (appstore / full) swap source dirs and deps,
|
||||
# so each must be type-checked independently.
|
||||
- name: Compile shared Kotlin/Native (appstore distribution)
|
||||
run: ./gradlew :composeApp:compileKotlinIosArm64 :composeApp:compileKotlinIosSimulatorArm64 -Pnuvio.ios.distribution=appstore --stacktrace
|
||||
|
||||
- name: Compile shared Kotlin/Native (full distribution)
|
||||
run: ./gradlew :composeApp:compileKotlinIosArm64 :composeApp:compileKotlinIosSimulatorArm64 -Pnuvio.ios.distribution=full --stacktrace
|
||||
|
||||
- name: Build iOS app (Debug, simulator destination, unsigned)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
xcodebuild \
|
||||
-project iosApp/iosApp.xcodeproj \
|
||||
-scheme iosApp \
|
||||
-configuration Debug \
|
||||
-destination 'generic/platform=iOS Simulator' \
|
||||
-derivedDataPath build/ios-derived-pr \
|
||||
-skipPackagePluginValidation \
|
||||
-skipMacroValidation \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGN_IDENTITY="" \
|
||||
DEVELOPMENT_TEAM="" \
|
||||
build
|
||||
|
||||
# Desktop targets (macOS .dmg, Windows .exe, Linux) are not yet wired up in
|
||||
# composeApp/build.gradle.kts. Add a `jvm("desktop")` target with the Compose
|
||||
# Desktop plugin and matching `desktop` matrix jobs here once that lands.
|
||||
247
.github/workflows/pr-test-build.yml
vendored
Normal file
247
.github/workflows/pr-test-build.yml
vendored
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
name: PR test build
|
||||
|
||||
# Two flows in one workflow:
|
||||
# 1. When a PR is opened, the bot drops a greeting that tells the author
|
||||
# how to request a testable APK.
|
||||
# 2. When anyone comments `!test-build` on a PR, build the Android APK from
|
||||
# the PR head and reply with a download link. Maintainer comments get a
|
||||
# fully-configured build (runtime secrets injected); everyone else gets
|
||||
# an APK built with empty runtime config, so the binary can be installed
|
||||
# but cannot exfiltrate secrets.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
greet:
|
||||
name: Greet new PR
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Post greeting comment
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const body = [
|
||||
`### Thanks for the PR! :tada:`,
|
||||
``,
|
||||
`If you'd like a **testable Android APK** built from this branch, post a comment containing:`,
|
||||
``,
|
||||
'```',
|
||||
'!test-build',
|
||||
'```',
|
||||
``,
|
||||
`A maintainer triggering the build will produce a fully-configured APK. Builds triggered by non-maintainers run **without runtime config** (Supabase, Trakt, IntroDB, IMDB, community URLs are empty) so the binary is installable but cannot reach those services.`,
|
||||
].join('\n')
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body,
|
||||
})
|
||||
|
||||
test-build:
|
||||
name: Build APK for PR
|
||||
if: >-
|
||||
github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request != null &&
|
||||
startsWith(github.event.comment.body, '!test-build')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Acknowledge command (rocket reaction)
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: context.payload.comment.id,
|
||||
content: 'rocket',
|
||||
})
|
||||
|
||||
# author_association reflects the commenter's relationship to the BASE
|
||||
# repo (not the fork), which is exactly what we want for trust gating.
|
||||
# Trusted associations: repo owner, org member, or explicit collaborator.
|
||||
- name: Resolve PR metadata and trust level
|
||||
id: pr
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.issue.number,
|
||||
})
|
||||
const trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR']
|
||||
const trusted = trustedAssociations.includes(
|
||||
context.payload.comment.author_association
|
||||
)
|
||||
core.setOutput('sha', pr.data.head.sha)
|
||||
core.setOutput('short_sha', pr.data.head.sha.slice(0, 7))
|
||||
core.setOutput('repo_full_name', pr.data.head.repo.full_name)
|
||||
core.setOutput('trusted', trusted ? 'true' : 'false')
|
||||
core.info(`PR #${context.issue.number} head: ${pr.data.head.sha}`)
|
||||
core.info(`Commenter association: ${context.payload.comment.author_association} (trusted=${trusted})`)
|
||||
|
||||
- name: Checkout PR head
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ steps.pr.outputs.repo_full_name }}
|
||||
ref: ${{ steps.pr.outputs.sha }}
|
||||
submodules: recursive
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Make gradlew executable
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
# Runtime config only flows in for trusted commenters. Untrusted builds
|
||||
# configure the build script with no local.properties, so the generated
|
||||
# config classes hold empty strings — same behavior as a fresh clone.
|
||||
- name: Write runtime config to local.properties (trusted only)
|
||||
if: steps.pr.outputs.trusted == 'true'
|
||||
uses: ./.github/actions/write-runtime-config
|
||||
with:
|
||||
supabase-url: ${{ secrets.SUPABASE_URL }}
|
||||
supabase-anon-key: ${{ secrets.SUPABASE_ANON_KEY }}
|
||||
trakt-client-id: ${{ secrets.TRAKT_CLIENT_ID }}
|
||||
trakt-client-secret: ${{ secrets.TRAKT_CLIENT_SECRET }}
|
||||
trakt-redirect-uri: ${{ secrets.TRAKT_REDIRECT_URI }}
|
||||
introdb-api-url: ${{ secrets.INTRODB_API_URL }}
|
||||
imdb-ratings-api-base-url: ${{ secrets.IMDB_RATINGS_API_BASE_URL }}
|
||||
imdb-tapframe-api-base-url: ${{ secrets.IMDB_TAPFRAME_API_BASE_URL }}
|
||||
contributions-url: ${{ secrets.CONTRIBUTIONS_URL }}
|
||||
donations-base-url: ${{ secrets.DONATIONS_BASE_URL }}
|
||||
donations-donate-url: ${{ secrets.DONATIONS_DONATE_URL }}
|
||||
|
||||
- name: Build Android APK (full, debug)
|
||||
run: ./gradlew :composeApp:assembleFullDebug --stacktrace
|
||||
|
||||
- name: Stage APK with descriptive name
|
||||
id: stage
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p artifacts
|
||||
src=$(ls composeApp/build/outputs/apk/full/debug/composeApp-full-debug*.apk 2>/dev/null | head -n1 || true)
|
||||
if [[ -z "$src" ]]; then
|
||||
echo "::error::Built APK not found under composeApp/build/outputs/apk/full/debug/"
|
||||
ls -la composeApp/build/outputs/apk/full/debug/ || true
|
||||
exit 1
|
||||
fi
|
||||
apk_name="Nuvio-pr${PR_NUMBER}-${SHORT_SHA}-full-debug.apk"
|
||||
cp "$src" "artifacts/$apk_name"
|
||||
echo "apk_name=$apk_name" >> "$GITHUB_OUTPUT"
|
||||
ls -la artifacts/
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.issue.number }}
|
||||
SHORT_SHA: ${{ steps.pr.outputs.short_sha }}
|
||||
|
||||
- name: Upload APK artifact
|
||||
id: upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.stage.outputs.apk_name }}
|
||||
path: artifacts/*.apk
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
- name: Post download comment
|
||||
if: success()
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
TRUSTED: ${{ steps.pr.outputs.trusted }}
|
||||
APK_NAME: ${{ steps.stage.outputs.apk_name }}
|
||||
ARTIFACT_NAME: ${{ steps.stage.outputs.apk_name }}
|
||||
PR_SHORT_SHA: ${{ steps.pr.outputs.short_sha }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
with:
|
||||
script: |
|
||||
const trusted = process.env.TRUSTED === 'true'
|
||||
const apkName = process.env.APK_NAME
|
||||
const artifactName = process.env.ARTIFACT_NAME
|
||||
const runUrl = process.env.RUN_URL
|
||||
const prShortSha = process.env.PR_SHORT_SHA
|
||||
|
||||
const cfgBanner = trusted
|
||||
? ':white_check_mark: Built **with** runtime config — fully functional.'
|
||||
: ':warning: Built **without** runtime config — Supabase, Trakt, IntroDB, IMDB, and community URLs are empty. The app installs and runs but cannot reach those services. A maintainer can re-trigger by commenting `!test-build` for a fully-configured build.'
|
||||
|
||||
// --- Download section: GitHub workflow artifact (requires login) ---
|
||||
const downloadSection = [
|
||||
'### Download',
|
||||
'',
|
||||
`[**Open workflow run**](${runUrl}) → scroll to the **Artifacts** section → \`${artifactName}\``,
|
||||
'',
|
||||
'_Downloading workflow artifacts requires being signed in to GitHub._',
|
||||
].join('\n')
|
||||
|
||||
// --- Alternative: nightly.link (public, no GitHub login required) ---
|
||||
// To swap the download mechanism, delete `downloadSection` above and
|
||||
// uncomment the block below. nightly.link is a third-party rewriter
|
||||
// (https://nightly.link) that exposes workflow artifacts as
|
||||
// unauthenticated download URLs.
|
||||
//
|
||||
// const nightlyUrl =
|
||||
// `https://nightly.link/${context.repo.owner}/${context.repo.repo}` +
|
||||
// `/actions/runs/${context.runId}/${artifactName}.zip`
|
||||
// const downloadSection = [
|
||||
// '### Download',
|
||||
// '',
|
||||
// `[**\`${apkName}\` (zip)**](${nightlyUrl}) — public link via [nightly.link](https://nightly.link), no GitHub login required.`,
|
||||
// '',
|
||||
// `Alternative: [open workflow run](${runUrl}) and grab the artifact directly (login required).`,
|
||||
// ].join('\n')
|
||||
|
||||
const body = [
|
||||
'### Test build ready',
|
||||
'',
|
||||
cfgBanner,
|
||||
'',
|
||||
`Built from PR head \`${prShortSha}\`. Artifact retained for 14 days.`,
|
||||
'',
|
||||
downloadSection,
|
||||
].join('\n')
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body,
|
||||
})
|
||||
|
||||
- name: Post failure comment
|
||||
if: failure()
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
with:
|
||||
script: |
|
||||
const body = [
|
||||
'### Test build failed :x:',
|
||||
'',
|
||||
`The Android build for this PR did not complete. See [workflow run](${process.env.RUN_URL}) for logs.`,
|
||||
].join('\n')
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body,
|
||||
})
|
||||
333
.github/workflows/release.yml
vendored
Normal file
333
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare release metadata
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
tag: ${{ steps.version.outputs.tag }}
|
||||
is_prerelease: ${{ steps.version.outputs.is_prerelease }}
|
||||
steps:
|
||||
- name: Extract version from tag
|
||||
id: version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag="${GITHUB_REF##*/}"
|
||||
version="${tag#v}"
|
||||
if [[ "$version" == *-* ]]; then
|
||||
is_prerelease=true
|
||||
else
|
||||
is_prerelease=false
|
||||
fi
|
||||
{
|
||||
echo "tag=$tag"
|
||||
echo "version=$version"
|
||||
echo "is_prerelease=$is_prerelease"
|
||||
} >>"$GITHUB_OUTPUT"
|
||||
echo "Tag: $tag (version: $version, prerelease: $is_prerelease)"
|
||||
|
||||
android:
|
||||
name: Android release
|
||||
runs-on: ubuntu-latest
|
||||
needs: prepare
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Make gradlew executable
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
- name: Write runtime config to local.properties
|
||||
uses: ./.github/actions/write-runtime-config
|
||||
with:
|
||||
supabase-url: ${{ secrets.SUPABASE_URL }}
|
||||
supabase-anon-key: ${{ secrets.SUPABASE_ANON_KEY }}
|
||||
trakt-client-id: ${{ secrets.TRAKT_CLIENT_ID }}
|
||||
trakt-client-secret: ${{ secrets.TRAKT_CLIENT_SECRET }}
|
||||
trakt-redirect-uri: ${{ secrets.TRAKT_REDIRECT_URI }}
|
||||
introdb-api-url: ${{ secrets.INTRODB_API_URL }}
|
||||
imdb-ratings-api-base-url: ${{ secrets.IMDB_RATINGS_API_BASE_URL }}
|
||||
imdb-tapframe-api-base-url: ${{ secrets.IMDB_TAPFRAME_API_BASE_URL }}
|
||||
contributions-url: ${{ secrets.CONTRIBUTIONS_URL }}
|
||||
donations-base-url: ${{ secrets.DONATIONS_BASE_URL }}
|
||||
donations-donate-url: ${{ secrets.DONATIONS_DONATE_URL }}
|
||||
|
||||
- name: Configure Android release signing
|
||||
env:
|
||||
KEYSTORE_BASE64: ${{ secrets.NUVIO_RELEASE_KEYSTORE_BASE64 }}
|
||||
KEYSTORE_PASSWORD: ${{ secrets.NUVIO_RELEASE_STORE_PASSWORD }}
|
||||
KEY_ALIAS: ${{ secrets.NUVIO_RELEASE_KEY_ALIAS }}
|
||||
KEY_PASSWORD: ${{ secrets.NUVIO_RELEASE_KEY_PASSWORD }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -n "${KEYSTORE_BASE64:-}" ]]; then
|
||||
keystore_path="$RUNNER_TEMP/nuvio-release.keystore"
|
||||
printf '%s' "$KEYSTORE_BASE64" | base64 -d > "$keystore_path"
|
||||
store_password="$KEYSTORE_PASSWORD"
|
||||
key_alias="$KEY_ALIAS"
|
||||
key_password="$KEY_PASSWORD"
|
||||
echo "Using release keystore from secrets."
|
||||
else
|
||||
echo "::warning::Release signing secrets not set — generating an ephemeral debug-style keystore so the build can complete. APKs will not be installable over signed production builds."
|
||||
keystore_path="$RUNNER_TEMP/nuvio-fallback.keystore"
|
||||
store_password="android"
|
||||
key_alias="androiddebugkey"
|
||||
key_password="android"
|
||||
keytool -genkeypair -v \
|
||||
-keystore "$keystore_path" \
|
||||
-storepass "$store_password" \
|
||||
-keypass "$key_password" \
|
||||
-alias "$key_alias" \
|
||||
-keyalg RSA -keysize 2048 -validity 10000 \
|
||||
-dname "CN=Android Debug,O=Android,C=US"
|
||||
fi
|
||||
{
|
||||
echo ""
|
||||
echo "NUVIO_RELEASE_STORE_FILE=$keystore_path"
|
||||
echo "NUVIO_RELEASE_STORE_PASSWORD=$store_password"
|
||||
echo "NUVIO_RELEASE_KEY_ALIAS=$key_alias"
|
||||
echo "NUVIO_RELEASE_KEY_PASSWORD=$key_password"
|
||||
} >> local.properties
|
||||
|
||||
- name: Build per-ABI release APKs (full + playstore)
|
||||
run: |
|
||||
./gradlew \
|
||||
:composeApp:assembleFullRelease \
|
||||
:composeApp:assemblePlaystoreRelease \
|
||||
-Pnuvio.splitAbi=true \
|
||||
--stacktrace
|
||||
|
||||
- name: Stage and rename APK artifacts
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p artifacts
|
||||
for flavor in full playstore; do
|
||||
for abi in arm64-v8a armeabi-v7a x86_64; do
|
||||
src=$(ls "composeApp/build/outputs/apk/${flavor}/release/composeApp-${flavor}-${abi}-release"*.apk 2>/dev/null | head -n1 || true)
|
||||
if [[ -z "$src" ]]; then
|
||||
echo "::error::Missing APK for ${flavor}/${abi}"
|
||||
ls -la "composeApp/build/outputs/apk/${flavor}/release/" || true
|
||||
exit 1
|
||||
fi
|
||||
dest="artifacts/Nuvio-${VERSION}-android-${flavor}-${abi}.apk"
|
||||
cp "$src" "$dest"
|
||||
echo " $src -> $dest"
|
||||
done
|
||||
done
|
||||
ls -la artifacts/
|
||||
|
||||
- name: Upload Android APK artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: android-apks
|
||||
path: artifacts/*.apk
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
ios:
|
||||
name: iOS release
|
||||
runs-on: macos-15
|
||||
needs: prepare
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
NUVIO_IOS_DISTRIBUTION: full
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Select latest stable Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: latest-stable
|
||||
|
||||
- name: Make gradlew executable
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
- name: Write runtime config to local.properties
|
||||
uses: ./.github/actions/write-runtime-config
|
||||
with:
|
||||
supabase-url: ${{ secrets.SUPABASE_URL }}
|
||||
supabase-anon-key: ${{ secrets.SUPABASE_ANON_KEY }}
|
||||
trakt-client-id: ${{ secrets.TRAKT_CLIENT_ID }}
|
||||
trakt-client-secret: ${{ secrets.TRAKT_CLIENT_SECRET }}
|
||||
trakt-redirect-uri: ${{ secrets.TRAKT_REDIRECT_URI }}
|
||||
introdb-api-url: ${{ secrets.INTRODB_API_URL }}
|
||||
imdb-ratings-api-base-url: ${{ secrets.IMDB_RATINGS_API_BASE_URL }}
|
||||
imdb-tapframe-api-base-url: ${{ secrets.IMDB_TAPFRAME_API_BASE_URL }}
|
||||
contributions-url: ${{ secrets.CONTRIBUTIONS_URL }}
|
||||
donations-base-url: ${{ secrets.DONATIONS_BASE_URL }}
|
||||
donations-donate-url: ${{ secrets.DONATIONS_DONATE_URL }}
|
||||
|
||||
# The Release Kotlin/Native link of the iOS framework regularly needs
|
||||
# 8–10 GB of heap (Compose + Ktor + Supabase). Override at the user
|
||||
# level so we don't have to raise the project default for local devs.
|
||||
- name: Raise Gradle heap for Kotlin/Native release link
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$HOME/.gradle"
|
||||
cat >> "$HOME/.gradle/gradle.properties" <<'EOF'
|
||||
org.gradle.jvmargs=-Xmx10g -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=1g
|
||||
kotlin.daemon.jvmargs=-Xmx4g
|
||||
EOF
|
||||
# Make sure no stale daemon is hanging around with the old args.
|
||||
./gradlew --stop || true
|
||||
|
||||
- name: Archive iOS app (Release, unsigned)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
xcodebuild \
|
||||
-project iosApp/iosApp.xcodeproj \
|
||||
-scheme iosApp \
|
||||
-configuration Release \
|
||||
-destination 'generic/platform=iOS' \
|
||||
-archivePath build/Nuvio.xcarchive \
|
||||
-skipPackagePluginValidation \
|
||||
-skipMacroValidation \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGN_IDENTITY="" \
|
||||
DEVELOPMENT_TEAM="" \
|
||||
archive
|
||||
|
||||
- name: Repackage .app into unsigned .ipa
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p artifacts build/Payload
|
||||
app_path="build/Nuvio.xcarchive/Products/Applications/Nuvio.app"
|
||||
if [[ ! -d "$app_path" ]]; then
|
||||
echo "::error::Built .app not found at $app_path"
|
||||
ls -laR build/Nuvio.xcarchive || true
|
||||
exit 1
|
||||
fi
|
||||
cp -R "$app_path" build/Payload/
|
||||
ipa_name="Nuvio-${VERSION}-ios.ipa"
|
||||
(
|
||||
cd build
|
||||
zip -ry "$ipa_name" Payload >/dev/null
|
||||
)
|
||||
mv "build/$ipa_name" "artifacts/$ipa_name"
|
||||
ls -la artifacts/
|
||||
|
||||
- name: Upload iOS IPA artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ios-ipa
|
||||
path: artifacts/*.ipa
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
release:
|
||||
name: Publish GitHub release
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare, android, ios]
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Download Android artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: android-apks
|
||||
path: release-assets/
|
||||
|
||||
- name: Download iOS artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ios-ipa
|
||||
path: release-assets/
|
||||
|
||||
- name: List release assets
|
||||
run: ls -la release-assets/
|
||||
|
||||
- name: Compose release notes
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
TAG: ${{ needs.prepare.outputs.tag }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base="https://github.com/${REPO}/releases/download/${TAG}"
|
||||
link() {
|
||||
local file="$1"
|
||||
local label="$2"
|
||||
if [[ -f "release-assets/$file" ]]; then
|
||||
printf '[%s](%s/%s)' "$label" "$base" "$file"
|
||||
else
|
||||
printf '—'
|
||||
fi
|
||||
}
|
||||
{
|
||||
echo "## Nuvio ${VERSION}"
|
||||
echo
|
||||
echo "### Downloads"
|
||||
echo
|
||||
echo "| Architecture | Android (Full) | Android (Play Store) | iOS |"
|
||||
echo "| --- | --- | --- | --- |"
|
||||
printf '| arm64 | %s | %s | %s |\n' \
|
||||
"$(link "Nuvio-${VERSION}-android-full-arm64-v8a.apk" "APK")" \
|
||||
"$(link "Nuvio-${VERSION}-android-playstore-arm64-v8a.apk" "APK")" \
|
||||
"$(link "Nuvio-${VERSION}-ios.ipa" "IPA")"
|
||||
printf '| armeabi-v7a | %s | %s | — |\n' \
|
||||
"$(link "Nuvio-${VERSION}-android-full-armeabi-v7a.apk" "APK")" \
|
||||
"$(link "Nuvio-${VERSION}-android-playstore-armeabi-v7a.apk" "APK")"
|
||||
printf '| x86_64 | %s | %s | — |\n' \
|
||||
"$(link "Nuvio-${VERSION}-android-full-x86_64.apk" "APK")" \
|
||||
"$(link "Nuvio-${VERSION}-android-playstore-x86_64.apk" "APK")"
|
||||
echo
|
||||
echo "### Notes"
|
||||
echo
|
||||
echo "- **Android (Full)** is the sideload-distribution flavor with the full feature set. **Android (Play Store)** matches the Play Store-distribution flavor."
|
||||
echo "- The iOS .ipa is **unsigned** — install with AltStore, Sideloadly, or another sideloading tool."
|
||||
echo "- Desktop builds (Windows / macOS / Linux) are not yet published from this workflow."
|
||||
} > release-notes.md
|
||||
echo "--- release notes ---"
|
||||
cat release-notes.md
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: "Nuvio ${{ needs.prepare.outputs.version }}"
|
||||
tag_name: ${{ needs.prepare.outputs.tag }}
|
||||
body_path: release-notes.md
|
||||
prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }}
|
||||
files: |
|
||||
release-assets/*.apk
|
||||
release-assets/*.ipa
|
||||
fail_on_unmatched_files: true
|
||||
|
|
@ -191,7 +191,10 @@ val generatedRuntimeConfigDir = layout.buildDirectory.dir("generated/runtime-con
|
|||
|
||||
val generateRuntimeConfigs = tasks.register<GenerateRuntimeConfigsTask>("generateRuntimeConfigs") {
|
||||
outputDir.set(generatedRuntimeConfigDir)
|
||||
localPropertiesFile.set(rootProject.layout.projectDirectory.file("local.properties"))
|
||||
val localPropsFile = rootProject.file("local.properties")
|
||||
if (localPropsFile.exists()) {
|
||||
localPropertiesFile.set(localPropsFile)
|
||||
}
|
||||
appVersionName.set(releaseAppVersionName)
|
||||
appVersionCode.set(releaseAppVersionCode)
|
||||
}
|
||||
|
|
@ -378,6 +381,14 @@ android {
|
|||
}
|
||||
}
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
isEnable = providers.gradleProperty("nuvio.splitAbi").orNull == "true"
|
||||
reset()
|
||||
include("arm64-v8a", "armeabi-v7a", "x86_64")
|
||||
isUniversalApk = false
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit c10b71ab8b4d90796d4a795f775d337c29198ad0
|
||||
1
vendor/quickjs-kt
vendored
1
vendor/quickjs-kt
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 57ce096200ac36bceb4e1ee5b6ec411b12357eb8
|
||||
Loading…
Reference in a new issue