diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 5a72105c..15a9632c 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -13,6 +13,8 @@ name: android on: push: branches: [main] + # a release tag publishes the Maven artifacts (see the `publish` job) + tags: ['v*'] paths: - 'android/**' - '.github/workflows/android.yml' @@ -82,3 +84,34 @@ jobs: sleep 8 adb logcat -d AndroidRuntime:E '*:S' | tee /tmp/crash.log test ! -s /tmp/crash.log || { echo "the sample crashed on the emulator"; exit 1; } + + # Publish the Kotlin artifacts to GitHub Packages on a release tag, so a single `git tag vX.Y.Z` + # releases every platform at one version: release.yml publishes npm, SPM resolves the Swift tag, + # and this publishes com.fundamental:{fundamental-core,fundamental-compose}:X.Y.Z. The version is + # derived from the tag (-PreleaseVersion), never hardcoded, and auth uses the Actions GITHUB_TOKEN + # — no personal token, no laptop publish. + publish: + name: publish maven (release tag) + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - name: set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + - name: set up Android SDK + uses: android-actions/setup-android@v3 + - name: publish core + compose to GitHub Packages + working-directory: android + env: + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${GITHUB_REF_NAME#v}" + echo "publishing com.fundamental:*:$VERSION from tag $GITHUB_REF_NAME" + ./gradlew :fundamental-core:publish :fundamental-compose:publish -PreleaseVersion="$VERSION" diff --git a/CHANGELOG.md b/CHANGELOG.md index c6685198..4e156d9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ a git tag (see [RELEASING.md](RELEASING.md)). ## [Unreleased] +- **Android artifacts publish from CI on a release tag, at a tag-derived version.** `android.yml` gains a `publish` job (`if: refs/tags/v*`, `packages: write`, Actions `GITHUB_TOKEN` — no personal token) that runs `:fundamental-core:publish :fundamental-compose:publish`. The version is derived from the tag via `-PreleaseVersion` and defaults to a `SNAPSHOT` locally — the hardcoded `version = "0.9.5"` is gone, so a publish can never silently overwrite a release with different code. One `git tag vX.Y.Z` now releases every plane at one version: npm via `release.yml`, Swift via the SPM tag, Kotlin via this. Publication config verified against the local Maven repo before merge. - **Positioning made consistent site-wide.** The homepage rewrite changed the hero, title and description, but the footer — which renders on *every* page — still read "a readable behavior layer for host objects", so the homepage contradicted itself on the same screen. The `Base.astro` default title carried it too. Both now say "a physics engine for interfaces". The only surviving instance is the changelog entry above, which quotes the old phrase to explain why it was retired; historical records that cite a former term are left intact. - **Finished retiring the freeze vocabulary across `docs/canonical/`.** The earlier change corrected the authoritative sources but left the wording in nine other canonical documents, where "the freeze contract" and "the frozen surface" still described a promise the project had stopped making. All now say *protected* / *removal-protection*. Four uses of the word are deliberately untouched because they mean something else entirely — `Object.freeze`'d policy clones, "freeze the sim" for reduced motion, "frozen particles" in the poster render mode, and "frozen design history" for the planning archive. A pattern-matched sweep would have broken all four. diff --git a/android/.gitignore b/android/.gitignore index f0041560..2f403a5e 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -7,3 +7,4 @@ build/ .idea/ *.iml local.properties +gpr.key diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 06ac980d..13d11d15 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -9,3 +9,15 @@ plugins { id("org.jetbrains.kotlin.android") version "2.1.0" apply false id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" apply false } + +// Coordinates for the published Android/JVM artifacts (GitHub Packages). Mirrors the +// npm scope @fundamental-engine and the Swift package products. +// +// The version is DERIVED, never typed: CI passes -PreleaseVersion= on a +// release tag, so the published version always equals the git tag and can never silently +// overwrite a prior release with different code. A local build with no property is a SNAPSHOT, +// which GitHub Packages will not mistake for a release. +subprojects { + group = "com.fundamental" + version = (findProperty("releaseVersion") as String?) ?: "0.0.0-SNAPSHOT" +} diff --git a/android/fundamental-compose/build.gradle.kts b/android/fundamental-compose/build.gradle.kts index 34d51f07..b8534d93 100644 --- a/android/fundamental-compose/build.gradle.kts +++ b/android/fundamental-compose/build.gradle.kts @@ -6,9 +6,11 @@ plugins { id("com.android.library") id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.plugin.compose") + `maven-publish` } android { + publishing { singleVariant("release") } namespace = "com.fundamental.compose" compileSdk = 34 buildToolsVersion = "34.0.0" @@ -42,3 +44,16 @@ dependencies { implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7") implementation("androidx.core:core-ktx:1.13.1") } + +// ── Publishing ────────────────────────────────────────────────────────────── +apply(from = rootProject.file("gradle/github-packages.gradle.kts")) +afterEvaluate { + publishing { + publications { + create("release") { + from(components["release"]) // AGP's release AAR + POM (transitive core dep included) + artifactId = "fundamental-compose" + } + } + } +} diff --git a/android/fundamental-core/build.gradle.kts b/android/fundamental-core/build.gradle.kts index c28ea4b2..9eb934e5 100644 --- a/android/fundamental-core/build.gradle.kts +++ b/android/fundamental-core/build.gradle.kts @@ -5,6 +5,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { kotlin("jvm") kotlin("plugin.serialization") + `maven-publish` } dependencies { @@ -86,3 +87,14 @@ sourceSets.main { tasks.named("processResources") { dependsOn(syncRecipes) } + +// ── Publishing ────────────────────────────────────────────────────────────── +apply(from = rootProject.file("gradle/github-packages.gradle.kts")) +publishing { + publications { + create("maven") { + from(components["java"]) + artifactId = "fundamental-core" + } + } +} diff --git a/android/gradle/github-packages.gradle.kts b/android/gradle/github-packages.gradle.kts new file mode 100644 index 00000000..67ab3031 --- /dev/null +++ b/android/gradle/github-packages.gradle.kts @@ -0,0 +1,23 @@ +// Shared GitHub Packages target. Applied by the modules we publish (core, compose). +// Credentials come from Gradle properties, android/gpr.key, or the environment so +// nothing secret is committed: +// gpr.user/gpr.key, GITHUB_ACTOR (defaults to repo owner), GITHUB_TOKEN. +// Uses configure rather than the `publishing { }` accessor, +// which is not generated for scripts applied via apply(from = …). +val gprKeyFromFile = rootProject.rootDir.resolve("gpr.key") + .takeIf { it.exists() } + ?.readText() + ?.trim() + ?.takeIf { it.isNotEmpty() } +configure { + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/zachshallbetter/fundamental-engine") + credentials { + username = (findProperty("gpr.user") as String?) ?: System.getenv("GITHUB_ACTOR") ?: "zachshallbetter" + password = (findProperty("gpr.key") as String?) ?: gprKeyFromFile ?: System.getenv("GITHUB_TOKEN") + } + } + } +}