version: every surface can say which build it is, and two of them were lying
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Successful in 15s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m50s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m19s
Desktop (Tauri) / Update manifest (push) Successful in 5s
Android / Kotlin + Rust (APK) (push) Successful in 7m59s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Successful in 15s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m50s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m19s
Desktop (Tauri) / Update manifest (push) Successful in 5s
Android / Kotlin + Rust (APK) (push) Successful in 7m59s
Note 3127 §5 removed version tags, so an artifact's self-report is now the only
answer to "which build is this?" — and nothing exists to contradict it when it
is wrong. Three surfaces gain a dim build line: the foot of the web rail, the
login screen, and the foot of Sync on Android.
The login screen because "I can't sign in" is a bug report like any other, and
requiring an account to read a build number withholds it from exactly the people
who can't get past that page. `/api/config` is already public.
Two of the values it was going to show were wrong, which is the part worth
knowing about.
The DESKTOP reported `env!("CARGO_PKG_VERSION")` from `config_get` and from the
startup log. `cargo tauri build --config '{"version": ...}'` overrides
tauri.conf.json, not Cargo's own metadata — so both read the literal `0.2.0` in
Cargo.toml, on every build ever shipped. They now read a display version baked in
by the lane through `option_env!`, hoisted to the crate root because two readers
of one fact is how this repo keeps producing 2181-2183. Not the ordering key
either: `1.0.<minutes>` is the opaque value Tauri's updater compares and must
never be shown to a person, and `update.rs` still reads it because a comparator
is exactly what it is (rule 149).
The SERVER fell back to `__version__` when APP_VERSION was absent, so a server
run from a checkout reported `0.2.0` — a real-looking version naming no build
anybody could obtain. `__init__.py` already asserted the honest answer was
"APP_VERSION being missing, which app.py already handles"; it did not, and a
comment claiming a behaviour two files away is how that stayed true-sounding.
Now an explicit "unknown", with the packaging version left where "unknown" is
not a legal value.
Android reads the INSTALLED package's versionName rather than BuildConfig, so it
reports what is actually on the phone.
Everything renders "unknown" rather than blank when it cannot say. A blank looks
like a layout bug; a plausible default cannot be caught by anything.
build.rs gets `rerun-if-env-changed` for the baked value: cargo does not track an
`option_env!` variable on its own, and the desktop lane having no cache today is
what makes that easy to forget the day one is added.
This commit is contained in:
@@ -162,6 +162,14 @@ jobs:
|
|||||||
# separate value and arrives with the UI that shows it (#3181).
|
# separate value and arrives with the UI that shows it (#3181).
|
||||||
version="$(sh ../../packaging/version.sh key desktop)"
|
version="$(sh ../../packaging/version.sh key desktop)"
|
||||||
echo "Building desktop ordering key $version"
|
echo "Building desktop ordering key $version"
|
||||||
|
# The DISPLAY version, baked into the binary by `option_env!` (#3181).
|
||||||
|
# A different value for a different audience: this is the one a person
|
||||||
|
# quotes in a bug report, the key above is the one only a comparator
|
||||||
|
# sees. Exported rather than passed as a flag because the macro that
|
||||||
|
# reads it is in Rust source, not in Tauri's config.
|
||||||
|
THOUGHTSYNC_DISPLAY_VERSION="$(sh ../../packaging/version.sh display desktop)"
|
||||||
|
export THOUGHTSYNC_DISPLAY_VERSION
|
||||||
|
echo "Baking display version $THOUGHTSYNC_DISPLAY_VERSION"
|
||||||
cargo tauri build \
|
cargo tauri build \
|
||||||
--config '{"build":{"beforeBuildCommand":""}}' \
|
--config '{"build":{"beforeBuildCommand":""}}' \
|
||||||
--config "{\"version\":\"$version\"}" \
|
--config "{\"version\":\"$version\"}" \
|
||||||
@@ -347,6 +355,14 @@ jobs:
|
|||||||
# separate value and arrives with the UI that shows it (#3181).
|
# separate value and arrives with the UI that shows it (#3181).
|
||||||
version="$(sh ../../packaging/version.sh key desktop)"
|
version="$(sh ../../packaging/version.sh key desktop)"
|
||||||
echo "Building desktop ordering key $version"
|
echo "Building desktop ordering key $version"
|
||||||
|
# The DISPLAY version, baked into the binary by `option_env!` (#3181).
|
||||||
|
# A different value for a different audience: this is the one a person
|
||||||
|
# quotes in a bug report, the key above is the one only a comparator
|
||||||
|
# sees. Exported rather than passed as a flag because the macro that
|
||||||
|
# reads it is in Rust source, not in Tauri's config.
|
||||||
|
THOUGHTSYNC_DISPLAY_VERSION="$(sh ../../packaging/version.sh display desktop)"
|
||||||
|
export THOUGHTSYNC_DISPLAY_VERSION
|
||||||
|
echo "Baking display version $THOUGHTSYNC_DISPLAY_VERSION"
|
||||||
updater='{}'
|
updater='{}'
|
||||||
if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
|
if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
|
||||||
updater='{"bundle":{"createUpdaterArtifacts":true}}'
|
updater='{"bundle":{"createUpdaterArtifacts":true}}'
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -120,10 +121,43 @@ fun SyncScreen(
|
|||||||
onDismissRevokeNotice = onDismissRevokeNotice,
|
onDismissRevokeNotice = onDismissRevokeNotice,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BuildLine()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The build, dim, at the foot of Sync — the same thing the web UI puts at the
|
||||||
|
* bottom of its rail (#3181).
|
||||||
|
*
|
||||||
|
* Read from the INSTALLED package rather than from `BuildConfig`: this reports what
|
||||||
|
* is actually on the phone, which is the question a bug report is asking. It also
|
||||||
|
* needs no `buildFeatures.buildConfig`, which this module does not enable.
|
||||||
|
*
|
||||||
|
* Note 3127 §5 is why it is here at all. With version tags gone, an artifact's own
|
||||||
|
* self-report is the only answer to "which build is this?" — so it renders
|
||||||
|
* "unknown" rather than nothing when the name is absent, because a blank line looks
|
||||||
|
* like a layout bug and a plausible default cannot be caught by anything.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun BuildLine() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val unknown = stringResource(R.string.build_unknown)
|
||||||
|
val version =
|
||||||
|
remember(context) {
|
||||||
|
runCatching {
|
||||||
|
context.packageManager.getPackageInfo(context.packageName, 0).versionName
|
||||||
|
}.getOrNull() ?: unknown
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = version,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ───────────────────────────────── linked ─────────────────────────────────
|
// ───────────────────────────────── linked ─────────────────────────────────
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -200,4 +200,9 @@
|
|||||||
|
|
||||||
<!-- Errors -->
|
<!-- Errors -->
|
||||||
<string name="error_dismiss">Dismiss</string>
|
<string name="error_dismiss">Dismiss</string>
|
||||||
|
|
||||||
|
<!-- The build, at the foot of Sync. Never blank: an APK with no versionName is
|
||||||
|
a real state (a bare `gradlew assembleDebug` with no override) and saying
|
||||||
|
so is better than an empty line that reads as a layout bug. -->
|
||||||
|
<string name="build_unknown">unknown</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
|
// Cargo does NOT track an `option_env!` variable on its own — the macro is
|
||||||
|
// expanded at compile time and nothing records that the crate depends on it.
|
||||||
|
// So without this line, a cached `target/` would keep a binary reporting
|
||||||
|
// whatever version the previous build baked, and the footer would confidently
|
||||||
|
// name the wrong build. The desktop lane has no cache today, which is exactly
|
||||||
|
// why this is easy to forget the day one is added.
|
||||||
|
//
|
||||||
|
// See DISPLAY_VERSION in `src/commands/local.rs`.
|
||||||
|
println!("cargo::rerun-if-env-changed=THOUGHTSYNC_DISPLAY_VERSION");
|
||||||
|
|
||||||
tauri_build::build()
|
tauri_build::build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ pub fn config_get(db: State<'_, Db>) -> PublicConfig {
|
|||||||
PublicConfig {
|
PublicConfig {
|
||||||
site_name: "ThoughtSync".to_string(),
|
site_name: "ThoughtSync".to_string(),
|
||||||
allow_registration: false,
|
allow_registration: false,
|
||||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
// The build a person reads, baked at compile time — see crate::display_version
|
||||||
|
// for why this is neither CARGO_PKG_VERSION nor the updater's ordering key.
|
||||||
|
version: crate::display_version().to_string(),
|
||||||
enable_url_unfurl: false,
|
enable_url_unfurl: false,
|
||||||
trash_retention_days: retention_days.max(0) as u32,
|
trash_retention_days: retention_days.max(0) as u32,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,37 @@ mod commands;
|
|||||||
mod integration;
|
mod integration;
|
||||||
mod update;
|
mod update;
|
||||||
|
|
||||||
|
/// The build a PERSON reads, baked in by the desktop lane at compile time.
|
||||||
|
///
|
||||||
|
/// Lives at the crate root because it has two readers — `config_get`, which puts it
|
||||||
|
/// in the UI, and `log_environment`, which puts it in the log — and this repo has
|
||||||
|
/// spent several issues on one fact held in two places (2181, 2182, 2183).
|
||||||
|
///
|
||||||
|
/// `option_env!`, not `env!`: a local `cargo tauri build` sets nothing, and this has
|
||||||
|
/// to keep compiling. `None` becomes "unknown" at each call site rather than a
|
||||||
|
/// plausible-looking default — note 3127 §5 makes this string the only answer to
|
||||||
|
/// "which build is this?" now that there are no version tags, so there is nothing
|
||||||
|
/// left to contradict it if it lies. An honest "I cannot say" is the only safe wrong
|
||||||
|
/// answer.
|
||||||
|
///
|
||||||
|
/// NOT `CARGO_PKG_VERSION`, which both readers used to use, and which was wrong on
|
||||||
|
/// every build ever shipped: `cargo tauri build --config '{"version": ...}'`
|
||||||
|
/// overrides `tauri.conf.json`, not Cargo's own metadata, so the literal `0.2.0` in
|
||||||
|
/// Cargo.toml is what reached the UI and the log regardless of what was built.
|
||||||
|
///
|
||||||
|
/// NOT the ordering key either. That value — `1.0.<minutes>`, which the override
|
||||||
|
/// above does set — is the opaque value Tauri's updater compares; it lands in bundle
|
||||||
|
/// filenames and `latest.json` and must never be shown to a person (#3144). Two
|
||||||
|
/// values, two audiences. `update.rs` deliberately still reads the key, through
|
||||||
|
/// `app.package_info().version`, because a comparator is exactly what it is.
|
||||||
|
const DISPLAY_VERSION: Option<&str> = option_env!("THOUGHTSYNC_DISPLAY_VERSION");
|
||||||
|
|
||||||
|
/// The baked build, or the honest "I cannot say". The only way in — the const is
|
||||||
|
/// private so no caller can reach past the fallback.
|
||||||
|
pub(crate) fn display_version() -> &'static str {
|
||||||
|
DISPLAY_VERSION.unwrap_or("unknown")
|
||||||
|
}
|
||||||
|
|
||||||
// The store and the sync engine live in the shared `thoughtsync-core` crate, which
|
// The store and the sync engine live in the shared `thoughtsync-core` crate, which
|
||||||
// the Android client binds through uniffi (Scribe note 2730). Aliased to their old
|
// the Android client binds through uniffi (Scribe note 2730). Aliased to their old
|
||||||
// names so every call site below reads exactly as it did when they were modules of
|
// names so every call site below reads exactly as it did when they were modules of
|
||||||
@@ -219,8 +250,8 @@ fn log_event(level: String, message: String) {
|
|||||||
fn log_environment(app: &tauri::App) {
|
fn log_environment(app: &tauri::App) {
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
log::info!(
|
log::info!(
|
||||||
"ThoughtSync desktop v{} starting ({} {})",
|
"ThoughtSync desktop {} starting ({} {})",
|
||||||
env!("CARGO_PKG_VERSION"),
|
display_version(),
|
||||||
std::env::consts::OS,
|
std::env::consts::OS,
|
||||||
std::env::consts::ARCH,
|
std::env::consts::ARCH,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,6 +27,23 @@ const ui = useUiStore();
|
|||||||
// Sync is a desktop-app concern: the web build already IS the server's UI.
|
// Sync is a desktop-app concern: the web build already IS the server's UI.
|
||||||
const desktopApp = isDesktop();
|
const desktopApp = isDesktop();
|
||||||
|
|
||||||
|
// The build, for the dim line at the foot of the rail (#3181).
|
||||||
|
//
|
||||||
|
// NEVER BLANK. "unknown" is the honest answer when the value is missing, and an
|
||||||
|
// empty space is a bug that reads as a design choice. Note 3127 §5: with version
|
||||||
|
// tags gone this is the only answer to "which build is this?", so it has to be
|
||||||
|
// either right or visibly absent.
|
||||||
|
//
|
||||||
|
// One slot, two artifacts, and that is deliberate rather than sloppy. In the
|
||||||
|
// browser `repo` is `rest`, so this is the SERVER's version; in the desktop shell
|
||||||
|
// `repo` is `local` and `config_get` returns the desktop build's own. Each surface
|
||||||
|
// names the thing the person is actually looking at. A linked server's version is
|
||||||
|
// a different question and Sync answers it separately.
|
||||||
|
const buildVersion = computed(() => config.version || "unknown");
|
||||||
|
const buildLabel = computed(
|
||||||
|
() => `ThoughtSync ${desktopApp ? "desktop" : "server"} build ${buildVersion.value}`,
|
||||||
|
);
|
||||||
|
|
||||||
async function removeView(f: SavedFilter) {
|
async function removeView(f: SavedFilter) {
|
||||||
if (!window.confirm(`Delete the "${f.name}" view?`)) return;
|
if (!window.confirm(`Delete the "${f.name}" view?`)) return;
|
||||||
try {
|
try {
|
||||||
@@ -415,7 +432,7 @@ async function signOut() {
|
|||||||
@click="drawer = false"
|
@click="drawer = false"
|
||||||
></div>
|
></div>
|
||||||
<aside
|
<aside
|
||||||
class="fixed inset-y-0 left-0 z-40 w-64 -translate-x-full overflow-y-auto border-r border-neutral-200 bg-neutral-50 p-3 pb-[calc(0.75rem+env(safe-area-inset-bottom,0px))] pt-[calc(0.75rem+env(safe-area-inset-top,0px))] transition-transform duration-200 sm:static sm:z-auto sm:w-56 sm:translate-x-0 sm:pb-3 sm:pt-3 dark:border-neutral-800 dark:bg-neutral-950"
|
class="fixed inset-y-0 left-0 z-40 flex w-64 -translate-x-full flex-col overflow-y-auto border-r border-neutral-200 bg-neutral-50 p-3 pb-[calc(0.75rem+env(safe-area-inset-bottom,0px))] pt-[calc(0.75rem+env(safe-area-inset-top,0px))] transition-transform duration-200 sm:static sm:z-auto sm:w-56 sm:translate-x-0 sm:pb-3 sm:pt-3 dark:border-neutral-800 dark:bg-neutral-950"
|
||||||
:class="drawer ? 'translate-x-0' : ''"
|
:class="drawer ? 'translate-x-0' : ''"
|
||||||
>
|
>
|
||||||
<nav class="flex flex-col gap-0.5 text-sm" @click="drawer = false">
|
<nav class="flex flex-col gap-0.5 text-sm" @click="drawer = false">
|
||||||
@@ -527,6 +544,17 @@ async function signOut() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<!-- The build. `mt-auto` puts it at the foot of the rail when the nav is
|
||||||
|
short and lets it simply follow when the nav has scrolled.
|
||||||
|
`select-all` because the one thing anybody does with this is copy it
|
||||||
|
into a bug report. -->
|
||||||
|
<p
|
||||||
|
class="mt-auto select-all px-3 pt-6 text-[11px] text-neutral-400 dark:text-neutral-500"
|
||||||
|
:title="buildLabel"
|
||||||
|
>
|
||||||
|
{{ buildVersion }}
|
||||||
|
</p>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- tabindex="-1" so the skip link above actually moves FOCUS here, not just
|
<!-- tabindex="-1" so the skip link above actually moves FOCUS here, not just
|
||||||
|
|||||||
@@ -88,6 +88,18 @@ async function submit() {
|
|||||||
>Create one</RouterLink
|
>Create one</RouterLink
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<!-- The build, on the one screen a person can reach WITHOUT an account.
|
||||||
|
"I can't sign in" is a bug report like any other and it needs a build
|
||||||
|
number; requiring a login to read one would withhold it from exactly the
|
||||||
|
people who cannot get past this page. `/api/config` is public, so this
|
||||||
|
costs nothing that was not already public (#3181). -->
|
||||||
|
<p
|
||||||
|
class="mt-8 select-all text-center text-[11px] text-neutral-400 dark:text-neutral-500"
|
||||||
|
:title="`ThoughtSync server build ${config.version || 'unknown'}`"
|
||||||
|
>
|
||||||
|
{{ config.version || "unknown" }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
"""ThoughtSync — self-hosted personal thought-capture web app (FabledSword family)."""
|
"""ThoughtSync — self-hosted personal thought-capture web app (FabledSword family)."""
|
||||||
|
|
||||||
# The FALLBACK version, used only when APP_VERSION is absent from the environment —
|
# PACKAGING METADATA, and nothing else. Not the version any running server reports.
|
||||||
# i.e. running from a checkout rather than from an image. A built image always has
|
#
|
||||||
# it, derived from the server's own shipped file set (packaging/version.sh), so this
|
# A built image carries APP_VERSION in the environment, derived from the server's
|
||||||
# string never reaches a deployed instance and bumping it changes nothing a user
|
# own shipped file set (packaging/version.sh); `app.py` reads that and reports an
|
||||||
# sees. Kept because a package needs a version and "unknown" is not a valid one for
|
# explicit "unknown" when it is absent, so this string never reaches a user and
|
||||||
# packaging metadata; the honest "I cannot say" for a running server is APP_VERSION
|
# bumping it changes nothing anybody sees.
|
||||||
# being missing, which app.py already handles.
|
#
|
||||||
|
# It exists because a Python package needs a version and "unknown" is not a legal
|
||||||
|
# one here. It used to double as app.py's fallback, which meant a server run from a
|
||||||
|
# checkout confidently reported `0.2.0` — a real-looking version naming no build
|
||||||
|
# that exists. Note 3127 §5 is why that matters more than it reads: with version
|
||||||
|
# tags gone, a build's self-report is the only answer to "which build is this?",
|
||||||
|
# and there is nothing left to catch it lying.
|
||||||
__version__ = "0.2.0"
|
__version__ = "0.2.0"
|
||||||
|
|||||||
+14
-2
@@ -11,7 +11,6 @@ from datetime import timedelta
|
|||||||
from quart import Quart, jsonify, send_from_directory
|
from quart import Quart, jsonify, send_from_directory
|
||||||
from quart.sessions import SecureCookieSessionInterface
|
from quart.sessions import SecureCookieSessionInterface
|
||||||
|
|
||||||
from . import __version__
|
|
||||||
from .auth import bp as auth_bp
|
from .auth import bp as auth_bp
|
||||||
from .client_dist import advertisement as client_advertisement, bp as client_bp
|
from .client_dist import advertisement as client_advertisement, bp as client_bp
|
||||||
from .config import Config
|
from .config import Config
|
||||||
@@ -64,7 +63,20 @@ def create_app() -> Quart:
|
|||||||
# Ephemeral/env secret so the app (and DB-free unit tests) construct without a
|
# Ephemeral/env secret so the app (and DB-free unit tests) construct without a
|
||||||
# database. before_serving swaps in the real, DB-persisted key before serving.
|
# database. before_serving swaps in the real, DB-persisted key before serving.
|
||||||
app.secret_key = Config.secret_key_env() or secrets.token_urlsafe(48)
|
app.secret_key = Config.secret_key_env() or secrets.token_urlsafe(48)
|
||||||
app.config["APP_VERSION"] = os.environ.get("APP_VERSION", __version__)
|
# The RUNNING build, or an explicit "unknown" — never the packaging fallback.
|
||||||
|
#
|
||||||
|
# This read `os.environ.get("APP_VERSION", __version__)`, so a server started
|
||||||
|
# from a checkout reported `0.2.0`: a real-looking version that names no build
|
||||||
|
# anybody could get. `__init__.py` already claimed the honest answer was
|
||||||
|
# "APP_VERSION being missing, which app.py already handles" — it did not, and a
|
||||||
|
# comment asserting a behaviour two files away from the code is how that stayed
|
||||||
|
# true-sounding for months.
|
||||||
|
#
|
||||||
|
# It matters more than it used to. Note 3127 §5 removed version tags, so this
|
||||||
|
# string is the only answer to "which build is this?" and nothing exists to
|
||||||
|
# contradict it when it is wrong. `__version__` stays where it belongs, as
|
||||||
|
# packaging metadata, which is the one place "unknown" is not a legal value.
|
||||||
|
app.config["APP_VERSION"] = os.environ.get("APP_VERSION") or "unknown"
|
||||||
app.config["SESSION_COOKIE_HTTPONLY"] = True
|
app.config["SESSION_COOKIE_HTTPONLY"] = True
|
||||||
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
|
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
|
||||||
# Auto-mark the session cookie Secure on HTTPS requests (see the interface above).
|
# Auto-mark the session cookie Secure on HTTPS requests (see the interface above).
|
||||||
|
|||||||
@@ -27,3 +27,50 @@ async def test_unknown_api_route_404s(app):
|
|||||||
client = app.test_client()
|
client = app.test_client()
|
||||||
resp = await client.get("/api/does-not-exist")
|
resp = await client.get("/api/does-not-exist")
|
||||||
assert resp.status_code == 404
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
# --- the version a running server reports ------------------------------------
|
||||||
|
#
|
||||||
|
# Note 3127 §5 removed version tags, so this string is the only answer to "which
|
||||||
|
# build is this?" and nothing exists to contradict it when it is wrong. That makes
|
||||||
|
# the FALLBACK the interesting case rather than the happy path: it used to be
|
||||||
|
# `__version__`, so a server run from a checkout reported `0.2.0` — a real-looking
|
||||||
|
# version naming no build anybody could obtain.
|
||||||
|
|
||||||
|
|
||||||
|
async def reported_version() -> str:
|
||||||
|
"""What a freshly built app tells /api/health it is.
|
||||||
|
|
||||||
|
Built per call rather than through the `app` fixture: the value is read from the
|
||||||
|
environment in `create_app`, so an app constructed before `monkeypatch` ran would
|
||||||
|
answer about the wrong environment.
|
||||||
|
"""
|
||||||
|
client = create_app().test_client()
|
||||||
|
return (await (await client.get("/api/health")).get_json())["version"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_the_version_is_whatever_the_environment_says(monkeypatch):
|
||||||
|
monkeypatch.setenv("APP_VERSION", "2026.08.29.0443")
|
||||||
|
assert await reported_version() == "2026.08.29.0443"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_no_version_in_the_environment_reports_unknown(monkeypatch):
|
||||||
|
"""The honest "I cannot say", not a plausible default.
|
||||||
|
|
||||||
|
Also asserted against `__version__` by name rather than against the literal it
|
||||||
|
happens to hold, so bumping the packaging version cannot make this pass for the
|
||||||
|
wrong reason.
|
||||||
|
"""
|
||||||
|
from thoughtsync import __version__
|
||||||
|
|
||||||
|
monkeypatch.delenv("APP_VERSION", raising=False)
|
||||||
|
reported = await reported_version()
|
||||||
|
assert reported == "unknown"
|
||||||
|
assert reported != __version__
|
||||||
|
|
||||||
|
|
||||||
|
async def test_an_empty_version_reports_unknown_too(monkeypatch):
|
||||||
|
"""`APP_VERSION=` is what a mis-set build arg looks like, and an empty string
|
||||||
|
renders as a blank space rather than as a missing value."""
|
||||||
|
monkeypatch.setenv("APP_VERSION", "")
|
||||||
|
assert await reported_version() == "unknown"
|
||||||
|
|||||||
Reference in New Issue
Block a user