Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@ concurrency:
permissions:
contents: read

# reference-app/build.gradle.kts reads these via `authProp`, which prefers an env
# var, then local.properties (git-ignored, so absent on a runner), then a hardcoded
# default. Without this block every CI build silently shipped the defaults. Declared
# at workflow level so all four build jobs inherit them; Actions has no YAML anchors.
#
# An unset variable expands to "" and `nonBlankEnv` treats blank as absent, so a
# missing value falls back to the default rather than baking an empty string. That
# also makes a typo'd variable name fail silently - hence the preflight check below.
env:
OAUTH_ISSUER: ${{ vars.OAUTH_ISSUER }}
OAUTH_CLIENT_ID: ${{ vars.OAUTH_CLIENT_ID }}
OAUTH_SCOPES: ${{ vars.OAUTH_SCOPES }}
OAUTH_REDIRECT_SCHEME: ${{ vars.OAUTH_REDIRECT_SCHEME }}
OAUTH_REDIRECT_HOST: ${{ vars.OAUTH_REDIRECT_HOST }}
OAUTH_DESKTOP_REDIRECT_PORT: ${{ vars.OAUTH_DESKTOP_REDIRECT_PORT }}
OAUTH_WEB_REDIRECT_URL: ${{ vars.OAUTH_WEB_REDIRECT_URL }}
FHIR_BASE_URL: ${{ vars.FHIR_BASE_URL }}

jobs:
preflight:
name: Preflight
Expand Down Expand Up @@ -85,6 +103,26 @@ jobs:
echo "signing_enabled=$SIGNING_ENABLED"
} >> "$GITHUB_OUTPUT"

- name: Verify required app config variables are set
run: |
missing=()
for v in \
OAUTH_ISSUER \
OAUTH_CLIENT_ID \
OAUTH_SCOPES \
OAUTH_REDIRECT_SCHEME \
OAUTH_REDIRECT_HOST \
OAUTH_DESKTOP_REDIRECT_PORT \
OAUTH_WEB_REDIRECT_URL \
FHIR_BASE_URL
do
[ -z "${!v}" ] && missing+=("$v")
done
if [ ${#missing[@]} -gt 0 ]; then
echo "::error::Missing repository variables: ${missing[*]}. Set them under Settings > Secrets and variables > Actions > Variables, or the build will silently fall back to the placeholder defaults in reference-app/build.gradle.kts."
exit 1
fi

- name: Verify required Android signing secrets are set
if: steps.parse.outputs.signing_enabled == 'true'
env:
Expand Down
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Each platform uses its idiomatic flow: Chrome Custom Tabs on Android, `ASWebAuth

Two things to know:

- **Changing the scheme or host means editing [`AndroidManifest.xml`](./reference-app/src/androidMain/AndroidManifest.xml) too** — the `LoginRedirectActivity` intent-filter hardcodes the pair.
- **The Android intent-filter tracks these automatically** — the build injects `OAUTH_REDIRECT_SCHEME` / `OAUTH_REDIRECT_HOST` into [`AndroidManifest.xml`](./reference-app/src/androidMain/AndroidManifest.xml) as manifest placeholders, so the `LoginRedirectActivity` deep link cannot drift from the generated config. Changing them still means registering the new redirect URI at your provider.
- Android Studio writes `sdk.dir` into the same `local.properties`. Keep it; Android and `allTests` builds need it.

### Keycloak example
Expand Down Expand Up @@ -339,6 +339,23 @@ Click **Use this template → Create a new repository** on GitHub, then work thr

Pushing a semantic version tag (`vX.Y.Z` or `vX.Y.Z-suffix`) triggers [`release.yml`](./.github/workflows/release.yml), which builds and signs every platform and publishes a GitHub Release with checksummed artifacts: an Android APK, desktop installers (`.deb`, `.rpm`, `.msi`, `.dmg`) and a portable Linux tarball. A `workflow_dispatch` run is a dry run — it builds and uploads artifacts but publishes no Release. The web and GitHub Pages jobs are gated off (`if: false`) pending a larger runner; the web preview is deployed manually meanwhile.

### App configuration on CI

`local.properties` is git-ignored, so it does not exist on a runner. Release builds
read the same keys from environment variables instead, which `release.yml` maps from
**repository variables** in a workflow-level `env:` block. Repository variables are not
environment variables on their own — they live only in the `vars` context and must be
mapped explicitly, or the build silently falls back to the placeholder defaults.

Set these under **Settings → Secrets and variables → Actions → Variables**:

`OAUTH_ISSUER`, `OAUTH_CLIENT_ID`, `OAUTH_SCOPES`, `OAUTH_REDIRECT_SCHEME`,
`OAUTH_REDIRECT_HOST`, `OAUTH_DESKTOP_REDIRECT_PORT`, `OAUTH_WEB_REDIRECT_URL`,
`FHIR_BASE_URL`.

The `Preflight` job fails the run if any of them is unset, so a typo surfaces in the
first minute rather than in a shipped artifact.

Local installers:

```shell
Expand Down
5 changes: 3 additions & 2 deletions local.properties.sample
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ OAUTH_SCOPES=openid profile email offline_access

# Android and iOS: a custom-scheme deep link, {SCHEME}://{HOST}.
# Register: dev.ohs.player.reference.app://auth
# Changing these also means editing the LoginRedirectActivity intent-filter in
# reference-app/src/androidMain/AndroidManifest.xml, which hardcodes the pair.
# The Android intent-filter picks these up automatically: the build injects them
# into AndroidManifest.xml as manifest placeholders, so the LoginRedirectActivity
# deep link always matches whatever is configured here.
OAUTH_REDIRECT_SCHEME=dev.ohs.player.reference.app
OAUTH_REDIRECT_HOST=auth

Expand Down
38 changes: 30 additions & 8 deletions reference-app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -221,18 +221,36 @@ val localProperties: Map<String, String> =
}
.getOrElse(emptyMap())

/*
* Resolved once at configuration time. `generateAuthConfig` bakes these into
* GeneratedAuthConfig, and the redirect scheme/host additionally drive the Android
* manifest placeholders below, so the LoginRedirectActivity intent-filter cannot
* drift from the config the app actually authenticates with.
*/
val resolvedIssuer = authProp("OAUTH_ISSUER", "https://keycloak.example.org/realms/ohs-player")
val resolvedClientId = authProp("OAUTH_CLIENT_ID", "ohs-player-reference-app")
val resolvedRedirectScheme = authProp("OAUTH_REDIRECT_SCHEME", "dev.ohs.player.reference.app")
val resolvedRedirectHost = authProp("OAUTH_REDIRECT_HOST", "auth")
val resolvedWebRedirectUrl = authProp("OAUTH_WEB_REDIRECT_URL", "http://localhost:8080/callback")
val resolvedDesktopPort = authProp("OAUTH_DESKTOP_REDIRECT_PORT", "8765")
val resolvedScopes = authProp("OAUTH_SCOPES", "openid profile email offline_access")
val resolvedFhirBaseUrl = authProp("FHIR_BASE_URL", "https://hapi.fhir.org/baseR4")

val authConfigOutputDir = layout.buildDirectory.dir("generated/authconfig/commonMain/kotlin")

val generateAuthConfig =
tasks.register("generateAuthConfig") {
val issuer = authProp("OAUTH_ISSUER", "https://keycloak.example.org/realms/ohs-player")
val clientId = authProp("OAUTH_CLIENT_ID", "ohs-player-reference-app")
val redirectScheme = authProp("OAUTH_REDIRECT_SCHEME", "dev.ohs.player.reference.app")
val redirectHost = authProp("OAUTH_REDIRECT_HOST", "auth")
val webRedirectUrl = authProp("OAUTH_WEB_REDIRECT_URL", "http://localhost:8080/callback")
val desktopPort = authProp("OAUTH_DESKTOP_REDIRECT_PORT", "8765")
val scopes = authProp("OAUTH_SCOPES", "openid profile email offline_access")
val fhirBaseUrl = authProp("FHIR_BASE_URL", "https://hapi.fhir.org/baseR4")
// Copied into locals so the doLast lambda below closes over plain Strings.
// Reading a script-level `val` from inside doLast captures a Gradle script
// object reference, which the configuration cache cannot serialize.
val issuer = resolvedIssuer
val clientId = resolvedClientId
val redirectScheme = resolvedRedirectScheme
val redirectHost = resolvedRedirectHost
val webRedirectUrl = resolvedWebRedirectUrl
val desktopPort = resolvedDesktopPort
val scopes = resolvedScopes
val fhirBaseUrl = resolvedFhirBaseUrl
val versionName = releaseVersionName
val versionCode = releaseVersionCode
val outDir = authConfigOutputDir
Expand Down Expand Up @@ -306,6 +324,10 @@ android {
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = releaseVersionCode
versionName = releaseVersionName
// Substituted into the LoginRedirectActivity intent-filter in AndroidManifest.xml,
// from the same values generateAuthConfig bakes into GeneratedAuthConfig.
manifestPlaceholders["oauthRedirectScheme"] = resolvedRedirectScheme
manifestPlaceholders["oauthRedirectHost"] = resolvedRedirectHost
}
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
signingConfigs {
Expand Down
12 changes: 7 additions & 5 deletions reference-app/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@
</activity>

<!--
Captures the OAuth redirect. The scheme/host MUST match
OAUTH_REDIRECT_SCHEME / OAUTH_REDIRECT_HOST in local.properties
(defaults shown). If you change those, update this intent-filter too.
Captures the OAuth redirect. The scheme/host are injected as manifest
placeholders by reference-app/build.gradle.kts, from the same
OAUTH_REDIRECT_SCHEME / OAUTH_REDIRECT_HOST values (env var or
local.properties) that generateAuthConfig bakes into GeneratedAuthConfig.
Do not hardcode them here - they would silently drift.
-->
<activity
android:name=".auth.LoginRedirectActivity"
Expand All @@ -38,8 +40,8 @@
<category android:name="android.intent.category.BROWSABLE" />

<data
android:scheme="dev.ohs.player.reference.app"
android:host="auth" />
android:scheme="${oauthRedirectScheme}"
android:host="${oauthRedirectHost}" />
</intent-filter>
</activity>
</application>
Expand Down
Loading