summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-x.ci/ci.cjs18
-rw-r--r--.ci/ci.ts28
-rw-r--r--.dockerignore5
-rw-r--r--Dockerfile20
-rw-r--r--desktop/build.gradle.kts26
-rwxr-xr-xsite/assemble.sh78
-rw-r--r--site/nginx.conf20
7 files changed, 184 insertions, 11 deletions
diff --git a/.ci/ci.cjs b/.ci/ci.cjs
index f7a5dec..f903738 100755
--- a/.ci/ci.cjs
+++ b/.ci/ci.cjs
@@ -5019,6 +5019,16 @@ var isPipeline = (t) => (0, import_pengueno3.isObject)(t) && "serialJobs" in t &
var REGISTRY = "img.liz.coffee";
var NAMESPACE = "emprespresso";
var IMG = "rookery";
+var getReleaseDeployment = (branch) => {
+ switch (branch) {
+ case "release":
+ return "https://plane.liz.coffee/api/v1/deploy?uuid=vzhqmprh2hqbjjcqrzxwkzww&force=false";
+ case "main":
+ return "https://plane.liz.coffee/api/v1/deploy?uuid=0hgnqywenjjcfltl6hoxmro0&force=false";
+ default:
+ return null;
+ }
+};
var getPipeline = () => {
const gitHookPipeline = new DefaultGitHookPipelineBuilder();
const branch = gitHookPipeline.getBranch();
@@ -5038,6 +5048,14 @@ var getPipeline = () => {
}
};
gitHookPipeline.addStage({ parallelJobs: [build] });
+ const webhookUrl = getReleaseDeployment(branch);
+ if (webhookUrl === null)
+ return gitHookPipeline.build();
+ const deploy = {
+ type: "coolify_webhook.js",
+ arguments: { webhookUrl }
+ };
+ gitHookPipeline.addStage({ parallelJobs: [deploy] });
return gitHookPipeline.build();
};
var main = () => {
diff --git a/.ci/ci.ts b/.ci/ci.ts
index 7908462..c0c8597 100644
--- a/.ci/ci.ts
+++ b/.ci/ci.ts
@@ -1,14 +1,27 @@
#!/usr/bin/env node
-import { BuildDockerImageJob, DefaultGitHookPipelineBuilder } from "@emprespresso/ci_model";
+import { BuildDockerImageJob, CoolifyWebhookJob, DefaultGitHookPipelineBuilder } from "@emprespresso/ci_model";
const REGISTRY = "img.liz.coffee";
const NAMESPACE = "emprespresso";
const IMG = "rookery";
+/** Coolify deploy hooks: `release` is the public download site, `main` is the beta one. Other branches only build. */
+const getReleaseDeployment = (branch: string) => {
+ switch (branch) {
+ case "release":
+ return "https://plane.liz.coffee/api/v1/deploy?uuid=vzhqmprh2hqbjjcqrzxwkzww&force=false";
+ case "main":
+ return "https://plane.liz.coffee/api/v1/deploy?uuid=0hgnqywenjjcfltl6hoxmro0&force=false";
+ default:
+ return null;
+ }
+};
+
/**
- * Every push: build the Dockerfile's `rookery` target, which runs the whole test suite
- * (`gradlew check`) and packages the desktop app. A failing test fails the image build.
+ * Every push builds the Dockerfile's `rookery` target: the test suite runs, the apps are built, and the
+ * result is a small web server with a download page (a failing test fails the image build). Pushes to
+ * `main` and `release` then tell Coolify to pull the new image.
*/
const getPipeline = () => {
const gitHookPipeline = new DefaultGitHookPipelineBuilder();
@@ -30,6 +43,15 @@ const getPipeline = () => {
};
gitHookPipeline.addStage({ parallelJobs: [build] });
+ const webhookUrl = getReleaseDeployment(branch);
+ if (webhookUrl === null) return gitHookPipeline.build();
+
+ const deploy: CoolifyWebhookJob = {
+ type: "coolify_webhook.js",
+ arguments: { webhookUrl },
+ };
+ gitHookPipeline.addStage({ parallelJobs: [deploy] });
+
return gitHookPipeline.build();
};
diff --git a/.dockerignore b/.dockerignore
index d02da12..698688f 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -4,3 +4,8 @@
**/build
.ci/node_modules
.ci/dist
+local.properties
+.env
+.env.*
+*_secret
+*.secret
diff --git a/Dockerfile b/Dockerfile
index 30c099e..84e02b2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-# Build + test stage: a failing test fails the image build, which is what lizCI checks.
+# Build stage: tests and every download. A failing test fails the image build, which is what lizCI checks.
FROM eclipse-temurin:21-jdk AS build
WORKDIR /src
# Android SDK: core and ui have Android targets, so Gradle needs one even for the desktop build.
@@ -16,11 +16,15 @@ COPY desktop/build.gradle.kts desktop/
COPY android/build.gradle.kts android/
RUN ./gradlew --no-daemon -q help
COPY . .
-RUN ./gradlew --no-daemon :core:jvmTest :desktop:createDistributable
-
-# Artifact image: just the packaged Linux desktop app (with its bundled runtime).
-FROM alpine:3.20 AS rookery
+# Tests, then every download: the Linux app (with its runtime), the universal jar and the Android APK.
+RUN ./gradlew --no-daemon :core:jvmTest :desktop:createDistributable :desktop:universalJar :android:assembleDebug
+# The commit goes in last, so a new commit doesn't redo the build above's cache-able layers.
ARG COMMIT=unknown
-COPY --from=build /src/desktop/build/compose/binaries/main/app/rookery /opt/rookery
-RUN printf '%s' "$COMMIT" > /opt/rookery/commit
-CMD ["ls", "/opt/rookery"]
+RUN sh site/assemble.sh /site "$COMMIT"
+
+# The deployed image: a static download page and the files it links to.
+FROM nginx:alpine AS rookery
+COPY site/nginx.conf /etc/nginx/conf.d/default.conf
+RUN rm -f /usr/share/nginx/html/index.html /usr/share/nginx/html/50x.html
+COPY --from=build /site /usr/share/nginx/html
+EXPOSE 80
diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts
index 2705014..d72a319 100644
--- a/desktop/build.gradle.kts
+++ b/desktop/build.gradle.kts
@@ -41,3 +41,29 @@ tasks.withType<JavaExec>().configureEach {
environment.putAll(System.getenv().filterKeys { it.startsWith("XDG_") || it.startsWith("ROOKERY_") })
standardInput = System.`in` // so `--cube monitor` can take commands
}
+
+// One jar that runs anywhere: the app plus the native graphics libraries for every desktop platform.
+// (Compose's own uber jar only carries the natives of the machine that built it.)
+val universal: Configuration by configurations.creating { extendsFrom(configurations.runtimeClasspath.get()) }
+dependencies {
+ universal(compose.desktop.linux_x64)
+ universal(compose.desktop.linux_arm64)
+ universal(compose.desktop.macos_x64)
+ universal(compose.desktop.macos_arm64)
+ universal(compose.desktop.windows_x64)
+}
+
+interface ArchiveServices { @get:javax.inject.Inject val archives: ArchiveOperations }
+val archives = objects.newInstance<ArchiveServices>().archives // an injected service, so the configuration cache can store it
+
+tasks.register<Jar>("universalJar") {
+ group = "distribution"
+ archiveBaseName = "rookery"
+ archiveClassifier = "universal"
+ manifest { attributes["Main-Class"] = "coffee.liz.rookery.desktop.MainKt" }
+ duplicatesStrategy = DuplicatesStrategy.EXCLUDE
+ from(sourceSets.main.get().output)
+ val unzip = archives // a local, so the closure below captures the service and not the whole build script
+ from(universal.elements.map { files -> files.filter { it.asFile.name.endsWith(".jar") }.map { unzip.zipTree(it.asFile) } })
+ exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA", "META-INF/INDEX.LIST")
+}
diff --git a/site/assemble.sh b/site/assemble.sh
new file mode 100755
index 0000000..aed5b27
--- /dev/null
+++ b/site/assemble.sh
@@ -0,0 +1,78 @@
+#!/bin/sh
+# Lays out the download site from whatever the Gradle build produced.
+# usage: site/assemble.sh <output dir> [commit]
+# Anything that wasn't built is simply left off the page, so this also works on a partial local build.
+set -eu
+
+root=$(cd "$(dirname "$0")/.." && pwd)
+out=${1:?usage: assemble.sh <output dir> [commit]}
+commit=${2:-unknown}
+mkdir -p "$out"
+
+case $(uname -m) in x86_64|amd64) arch=x64 ;; aarch64|arm64) arch=arm64 ;; *) arch=$(uname -m) ;; esac
+sha() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@"; else shasum -a 256 "$@"; fi; }
+size() { du -h "$1" | cut -f1 | tr -d ' '; }
+
+apk=$root/android/build/outputs/apk/debug/android-debug.apk
+jar=$root/desktop/build/libs/rookery-universal.jar
+app=$root/desktop/build/compose/binaries/main/app/rookery
+
+[ -f "$apk" ] && cp "$apk" "$out/rookery-android.apk"
+[ -f "$jar" ] && cp "$jar" "$out/rookery.jar"
+[ -d "$app" ] && tar -C "$(dirname "$app")" -czf "$out/rookery-linux-$arch.tar.gz" rookery
+
+printf '%s\n' "$commit" > "$out/commit.txt"
+(cd "$out" && sha rookery* > SHA256SUMS)
+
+row() { # file, description
+ [ -f "$out/$1" ] && printf '<tr><td><a href="%s">%s</a></td><td>%s</td><td class="n">%s</td></tr>\n' "$1" "$1" "$2" "$(size "$out/$1")"
+ return 0
+}
+
+short=$(printf '%.7s' "$commit")
+if [ "$commit" = unknown ]; then rev="$short"; else rev="<a href=\"https://code.liz.coffee/rookery.git/commit/?id=$commit\">$short</a>"; fi
+built=$(date -u +"%Y-%m-%d %H:%M UTC")
+
+cat > "$out/index.html" <<HTML
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>rookery</title>
+<style>
+ :root { --bg: black; --fg: white; --a: #c4d0c8; --h: #7d978c; --line: #3a403d; --dim: #a3ada7; }
+ body { max-width: 800px; margin: 0 auto; padding: 16px; background: var(--bg); color: var(--fg); font-family: serif; line-height: 1.5; }
+ h1, h2 { color: var(--h); margin-bottom: 8px; }
+ a { color: var(--a); }
+ table { border-collapse: collapse; width: 100%; }
+ td { border: 1px solid var(--line); padding: 4px 8px; }
+ td.n { text-align: right; white-space: nowrap; color: var(--dim); }
+ tt, code { font-family: monospace; }
+ .dim { color: var(--dim); }
+ hr { border: 0; border-top: 1px solid var(--line); margin: 20px 0; }
+</style>
+</head>
+<body>
+<h1>rookery</h1>
+<p>speedcubing timer and blindfold trainer. desktop and android.</p>
+<hr>
+<h2>downloads</h2>
+<table>
+$(row rookery-android.apk "Android 8+, debug-signed")
+$(row "rookery-linux-$arch.tar.gz" "Linux $arch, runtime included")
+$(row rookery.jar "Java 21; Linux, macOS, Windows")
+</table>
+<ul>
+ <li>apk: allow installing from this source when asked.</li>
+ <li>tar.gz: extract, then run <tt>rookery/bin/rookery</tt>.</li>
+ <li>jar: <tt>java -jar rookery.jar</tt></li>
+ <li><a href="SHA256SUMS">SHA256SUMS</a></li>
+</ul>
+<hr>
+<p class="dim">build $rev, $built</p>
+</body>
+</html>
+HTML
+
+echo "site: $(ls "$out" | tr '\n' ' ')"
diff --git a/site/nginx.conf b/site/nginx.conf
new file mode 100644
index 0000000..310d68d
--- /dev/null
+++ b/site/nginx.conf
@@ -0,0 +1,20 @@
+server {
+ listen 80;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ # a new build replaces these files in place, so have clients check
+ add_header Cache-Control "no-cache";
+
+ location ~ \.apk$ {
+ default_type application/vnd.android.package-archive;
+ add_header Cache-Control "no-cache";
+ }
+ location = /SHA256SUMS {
+ default_type text/plain;
+ add_header Cache-Control "no-cache";
+ }
+ location / {
+ try_files $uri $uri/ =404;
+ }
+}