1 parent
d91212d24c
commit
dbe7103687
21 files changed
+182
-154
No files matched your search
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Static/offline release checks for RustBox 0.2.15.
|
||||
"""Static/offline release checks for RustBox 0.2.16.
|
||||
|
||||
These checks intentionally avoid pretending to replace `cargo test`: they validate
|
||||
configuration, SQL migrations, Docker examples, query-catalog parity and release
|
||||
@@ -49,14 +49,14 @@ def extract_section(text: str, begin: str, end: str) -> str:
|
||||
|
||||
# Release/toolchain metadata.
|
||||
cargo = tomllib.loads(read("Cargo.toml"))
|
||||
require(cargo["package"]["version"] == "0.2.15", "Cargo package version is 0.2.15")
|
||||
require(cargo["package"]["version"] == "0.2.16", "Cargo package version is 0.2.16")
|
||||
require(cargo["package"]["rust-version"] == "1.99.1", "Cargo rust-version is 1.99.1")
|
||||
toolchain = tomllib.loads(read("rust-toolchain.toml"))
|
||||
require(toolchain["toolchain"]["channel"] == "1.99.1", "rust-toolchain pins Rust 1.99.1")
|
||||
require((ROOT / "Cargo.lock").is_file(), "Cargo.lock is retained for reproducible application builds")
|
||||
lock = tomllib.loads(read("Cargo.lock"))
|
||||
lock_rustbox = next((pkg for pkg in lock.get("package", []) if pkg.get("name") == "rustbox"), {})
|
||||
require(lock_rustbox.get("version") == "0.2.15", "Cargo.lock RustBox package version is 0.2.15")
|
||||
require(lock_rustbox.get("version") == "0.2.16", "Cargo.lock RustBox package version is 0.2.16")
|
||||
require(cargo["dependencies"].get("encoding_rs") == "0.8", "direct charset decoder dependency is pinned")
|
||||
require("encoding_rs" in lock_rustbox.get("dependencies", []), "Cargo.lock records direct charset decoder dependency")
|
||||
require("cargo generate-lockfile" in read("scripts/build-release.sh"), "release script generates Cargo.lock when missing")
|
||||
@@ -71,10 +71,10 @@ require("make_zip.py" in package_script and "sha256sum" in package_script, "rele
|
||||
require("cargo generate-lockfile" in read(".github/workflows/ci.yml"), "CI regenerates Cargo.lock before --locked checks")
|
||||
require("validate-release-static.py" in read("scripts/build-release.sh"), "release script runs offline/static release gate")
|
||||
require("validate-release-static.py" in read(".github/workflows/ci.yml"), "CI runs offline/static release gate")
|
||||
require(read("README.md").startswith("# RustBox 0.2.15"), "README identifies RustBox 0.2.15")
|
||||
require(read("RELEASE-MANIFEST.txt").startswith("RustBox 0.2.15"), "release manifest identifies RustBox 0.2.15")
|
||||
require(read("RELEASE-VALIDATION.txt").startswith("RustBox 0.2.15"), "release validation identifies RustBox 0.2.15")
|
||||
require(read("docs/IMPLEMENTATION_STATUS.md").startswith("# RustBox 0.2.15"), "implementation status identifies RustBox 0.2.15")
|
||||
require(read("README.md").startswith("# RustBox 0.2.16"), "README identifies RustBox 0.2.16")
|
||||
require(read("RELEASE-MANIFEST.txt").startswith("RustBox 0.2.16"), "release manifest identifies RustBox 0.2.16")
|
||||
require(read("RELEASE-VALIDATION.txt").startswith("RustBox 0.2.16"), "release validation identifies RustBox 0.2.16")
|
||||
require(read("docs/IMPLEMENTATION_STATUS.md").startswith("# RustBox 0.2.16"), "implementation status identifies RustBox 0.2.16")
|
||||
bootstrap_js = read("src/web/js/00-bootstrap.js")
|
||||
bindings_js = read("src/web/js/95-bindings-boot.js")
|
||||
realtime_js = read("src/web/js/80-settings-realtime.js")
|
||||
@@ -111,7 +111,6 @@ require('id="compose-save-draft"' in read("src/web/index.html") and '$("#compose
|
||||
|
||||
require('rememberSignatureRange' in bindings_js and 'selectedSignatureRange' in bindings_js and 'if (!range || range.collapsed) return null;' in bindings_js and 'state.identitySignatureRange = null;' in bindings_js and '.signature-editor-toolbar button' in bindings_js and 'event.preventDefault();' in bindings_js and 'restoreEditorRange(range, signatureEditor);' in bindings_js, "signature formatting preserves real selections and refuses collapsed-caret commands that would enable sticky typing styles")
|
||||
index_html = read("src/web/index.html")
|
||||
require('<div class="folder-quota-label"><span>Mailbox storage</span>' in index_html and '<span id="mail-server-label">RustBox</span><span>__RUSTBOX_VERSION__</span>' in index_html, "mailbox storage and footer labels stay protocol-neutral")
|
||||
require('<div class="identity-signature-field"><span id="identity-signature-label">HTML signature / footer</span>' in index_html and 'aria-labelledby="identity-signature-label"' in index_html and '<label class="identity-signature-field">' not in index_html, "HTML signature editor is not wrapped in a label that can implicitly activate the Bold toolbar button")
|
||||
require('function ensureComposeEntryBeforeSignature' in read("src/web/js/72-openpgp-identities.js") and 'data-rustbox-signature-entry' in read("src/web/js/72-openpgp-identities.js") and 'compose-body-entry' in compose_css, "new visible signatures keep an editable compose body entry above the signature")
|
||||
require('.compose-signature.compose-signature-new' in compose_css and 'margin-top: 2.4em;' in compose_css and 'border-top: 1px solid color-mix(in srgb, var(--line) 58%, transparent);' in compose_css and 'removeAttribute("data-rustbox-signature-entry")' in compose_js, "new-message signature spacing is scoped, the separator is subdued, and editor-only scaffolding is stripped from outgoing HTML")
|
||||
@@ -141,8 +140,8 @@ require('encoding_rs::Encoding::for_label' in read("src/imap/parse.rs") and 'dec
|
||||
require('let is_pdf = response_content_type_is(&response, "application/pdf");' in read("src/routes/mod.rs") and 'HeaderValue::from_static("SAMEORIGIN")' in read("src/routes/mod.rs") and "HeaderValue::from_static(\"frame-ancestors 'self'\")" in read("src/routes/mod.rs"), "security headers allow same-origin framing for inline PDF previews only")
|
||||
index_html = read("src/web/index.html")
|
||||
require('class="compose-limit-summary"' in index_html and 'Per-file upload limit set with' not in index_html and 'Hard limit for the complete RFC822/MIME message' not in index_html, "Composing shows compact raw size limits without implementation details")
|
||||
require("V049" in read("RELEASE-MANIFEST.txt") and "V050" in read("RELEASE-MANIFEST.txt") and "V051" in read("RELEASE-MANIFEST.txt") and "V052" not in read("RELEASE-MANIFEST.txt"), "release manifest documents V049 baseline, V050 current migration and next V051")
|
||||
require("V049" in read("migrations/README.md") and "V050" in read("migrations/README.md") and "V051" in read("migrations/README.md") and "V052" not in read("migrations/README.md"), "migration guide documents V049 baseline, V050 current migration and next V051")
|
||||
require("V050" in read("RELEASE-MANIFEST.txt") and "V051" in read("RELEASE-MANIFEST.txt") and "V052" in read("RELEASE-MANIFEST.txt"), "release manifest documents V050 baseline, active V051 and next V052")
|
||||
require("V050" in read("migrations/README.md") and "V051" in read("migrations/README.md") and "V052" in read("migrations/README.md"), "migration guide documents V050 baseline, active V051 and next V052")
|
||||
require("versions = [50]" in read("scripts/migrate_roundcube.py"), "Roundcube provisioning fallback follows the V050 consolidated baseline")
|
||||
require((ROOT / "docs/STORAGE_SEARCH_OPERATIONS.md").is_file(), "storage/search operations guide exists")
|
||||
require('imap_host = ""' not in read("config.dev.toml"), "development config does not override IMAP host with an invalid empty value")
|
||||
@@ -552,64 +551,33 @@ try:
|
||||
require(sqlite_generation == ("2",), "SQLite header-sync UPSERT remains unambiguous and increments generation")
|
||||
account_default = conn.execute("PRAGMA table_info(rustbox_accounts)").fetchall()
|
||||
version_default = next((row[4] for row in account_default if row[1] == "schema_version"), None)
|
||||
require(str(version_default).strip("'\"") == "50", "fresh account schema version defaults to current V050")
|
||||
require(str(version_default).strip("'\"") == "50", "fresh global compatibility baseline is V050 before tenant registration")
|
||||
display_columns = {row[1]: row for row in conn.execute("PRAGMA table_info(u_test__display_preferences)")}
|
||||
require("adaptive_email_colors" in display_columns and str(display_columns["adaptive_email_colors"][4]) == "0", "V050 defaults adaptive HTML-mail colors off")
|
||||
require("adaptive_email_colors" in display_columns and str(display_columns["adaptive_email_colors"][4]) == "0", "fresh V051 schema defaults adaptive HTML-mail colors off")
|
||||
mail_columns = {row[1]: row for row in conn.execute("PRAGMA table_info(u_test__mail_preferences)")}
|
||||
require("message_loading_mode" in mail_columns and str(mail_columns["message_loading_mode"][4]).strip("\'\"") == "pagination", "V050 defaults mailbox loading mode to pagination")
|
||||
require("message_loading_mode" in mail_columns and str(mail_columns["message_loading_mode"][4]).strip("\'\"") == "pagination", "fresh V051 schema defaults mailbox loading mode to pagination")
|
||||
conn.close()
|
||||
except Exception as exc:
|
||||
errors.append(f"fresh SQLite schema execution failed: {exc}")
|
||||
|
||||
# V047-V049 are squashed into the V049 compatibility baseline. V050 is the single runtime migration.
|
||||
# V050 upgrade execution and migration/query parity across SQL engines.
|
||||
# V050 upgrades a V049-compatible account schema with all unreleased 0.2.15 preferences.
|
||||
try:
|
||||
conn = sqlite3.connect(":memory:")
|
||||
conn.executescript(
|
||||
"CREATE TABLE u_test__display_preferences (id INTEGER PRIMARY KEY);"
|
||||
"CREATE TABLE u_test__mail_preferences (id INTEGER PRIMARY KEY);"
|
||||
"CREATE TABLE u_test__identities (id INTEGER PRIMARY KEY);"
|
||||
)
|
||||
migration = read("migrations/sqlite/V050__compact_rows_and_signature_mode.sql").replace("{{p}}", "u_test__")
|
||||
conn.executescript(migration)
|
||||
display_columns = {row[1]: row for row in conn.execute("PRAGMA table_info(u_test__display_preferences)")}
|
||||
mail_columns = {row[1]: row for row in conn.execute("PRAGMA table_info(u_test__mail_preferences)")}
|
||||
identity_columns = {row[1]: row for row in conn.execute("PRAGMA table_info(u_test__identities)")}
|
||||
require("adaptive_email_colors" in display_columns and str(display_columns["adaptive_email_colors"][4]) == "0", "V050 defaults adaptive HTML-mail colors off in SQLite")
|
||||
require("message_loading_mode" in mail_columns and str(mail_columns["message_loading_mode"][4]).strip("\'\"") == "pagination", "V050 defaults mailbox loading mode to pagination in SQLite")
|
||||
require("compact_wide_rows" in mail_columns and str(mail_columns["compact_wide_rows"][4]) == "1", "V050 defaults compact wide rows on in SQLite")
|
||||
require("signature_insert_mode" in identity_columns and identity_columns["signature_insert_mode"][4] == "'silent'", "V050 defaults identity signature insertion to silent in SQLite")
|
||||
conn.execute("INSERT INTO u_test__display_preferences(id) VALUES (1)")
|
||||
conn.execute("INSERT INTO u_test__mail_preferences(id) VALUES (1)")
|
||||
conn.execute("INSERT INTO u_test__identities(id) VALUES (1)")
|
||||
require(conn.execute("SELECT adaptive_email_colors FROM u_test__display_preferences WHERE id=1").fetchone()[0] == 0, "V050 applies adaptive-color default to SQLite rows")
|
||||
require(conn.execute("SELECT message_loading_mode, compact_wide_rows FROM u_test__mail_preferences WHERE id=1").fetchone() == ("pagination", 1), "V050 applies mailbox pagination and compact-row defaults to SQLite rows")
|
||||
require(conn.execute("SELECT signature_insert_mode FROM u_test__identities WHERE id=1").fetchone()[0] == "silent", "V050 applies silent-signature default to SQLite rows")
|
||||
conn.close()
|
||||
except Exception as exc:
|
||||
errors.append(f"SQLite V050 migration execution failed: {exc}")
|
||||
|
||||
# V050 is the compatibility baseline. V051 is the active 0.2.16 migration.
|
||||
# Migration/query parity across SQL engines.
|
||||
active_migration = "V051__adaptive_email_colors_and_message_loading_mode.sql"
|
||||
for dialect in ("sqlite", "mysql", "pgsql"):
|
||||
for retired in (
|
||||
"V047__blob_keys_and_search_outbox.sql",
|
||||
"V048__folder_counter_generation.sql",
|
||||
"V049__blob_delete_outbox.sql",
|
||||
"V050__compact_rows_and_signature_mode.sql",
|
||||
"V051__adaptive_email_colors.sql",
|
||||
):
|
||||
require(not (ROOT / f"migrations/{dialect}/{retired}").exists(), f"consolidated/unreleased migration {retired} is absent for {dialect}")
|
||||
require((ROOT / f"migrations/{dialect}/V050__compact_rows_and_signature_mode.sql").is_file(), f"V050 runtime migration exists for {dialect}")
|
||||
migration_text = read(f"migrations/{dialect}/V050__compact_rows_and_signature_mode.sql")
|
||||
require(
|
||||
"adaptive_email_colors" in migration_text
|
||||
and "message_loading_mode" in migration_text
|
||||
and "compact_wide_rows" in migration_text
|
||||
and "signature_insert_mode" in migration_text,
|
||||
f"V050 runtime migration contains all unreleased preference columns for {dialect}",
|
||||
)
|
||||
require(not (ROOT / f"migrations/{dialect}/{retired}").exists(), f"retired migration {retired} is absent for {dialect}")
|
||||
require((ROOT / f"migrations/{dialect}/{active_migration}").is_file(), f"V051 runtime migration exists for {dialect}")
|
||||
migration_text = read(f"migrations/{dialect}/{active_migration}")
|
||||
require("adaptive_email_colors" in migration_text and "message_loading_mode" in migration_text and "pagination" in migration_text, f"V051 adds both 0.2.16 preferences for {dialect}")
|
||||
schema_text = read(f"migrations/{dialect}/schema.sql")
|
||||
require(
|
||||
"Squashed compatibility baseline: V049. Current schema: V050." in schema_text
|
||||
"Squashed compatibility baseline: V050; current schema: V051." in schema_text
|
||||
and "counter_generation" in schema_text
|
||||
and "blob_delete_outbox" in schema_text
|
||||
and "compact_wide_rows" in schema_text
|
||||
@@ -617,7 +585,7 @@ for dialect in ("sqlite", "mysql", "pgsql"):
|
||||
and "adaptive_email_colors" in schema_text
|
||||
and "message_loading_mode" in schema_text
|
||||
and "pagination" in schema_text,
|
||||
f"fresh {dialect} schema contains current V050 schema on the V049 compatibility baseline",
|
||||
f"fresh {dialect} schema contains V050 baseline plus current V051 columns",
|
||||
)
|
||||
queries = read(f"src/queries/{dialect}/queries.rs")
|
||||
require("adaptive_email_colors" in queries, f"display-preference query includes adaptive_email_colors for {dialect}")
|
||||
@@ -635,16 +603,52 @@ for dialect in ("sqlite", "mysql", "pgsql"):
|
||||
blob_qnames = set(re.findall(r"pub const (Q_\d{3})", blob_block.group(1)))
|
||||
require(blob_qnames == {f"Q_{i:03d}" for i in range(1, 6)}, f"blob deletion outbox Q001-Q005 parity for {dialect}")
|
||||
|
||||
# Execute the real SQLite V051 migration against a production-like V050 tenant.
|
||||
try:
|
||||
conn = sqlite3.connect(":memory:")
|
||||
conn.executescript("""
|
||||
CREATE TABLE u_prod__display_preferences (
|
||||
id INTEGER PRIMARY KEY,
|
||||
theme TEXT NOT NULL DEFAULT 'system',
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE u_prod__mail_preferences (
|
||||
id INTEGER PRIMARY KEY,
|
||||
messages_per_page INTEGER NOT NULL DEFAULT 300,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
INSERT INTO u_prod__display_preferences(id) VALUES(1);
|
||||
INSERT INTO u_prod__mail_preferences(id) VALUES(1);
|
||||
""")
|
||||
conn.executescript(read(f"migrations/sqlite/{active_migration}").replace("{{p}}", "u_prod__"))
|
||||
migrated_display = {row[1]: row for row in conn.execute("PRAGMA table_info(u_prod__display_preferences)")}
|
||||
migrated_mail = {row[1]: row for row in conn.execute("PRAGMA table_info(u_prod__mail_preferences)")}
|
||||
require("adaptive_email_colors" in migrated_display and str(migrated_display["adaptive_email_colors"][4]) == "0", "SQLite V051 adds adaptive_email_colors to a V050 tenant")
|
||||
require("message_loading_mode" in migrated_mail and str(migrated_mail["message_loading_mode"][4]).strip("'\"") == "pagination", "SQLite V051 adds pagination mode to a V050 tenant")
|
||||
values = conn.execute("SELECT adaptive_email_colors FROM u_prod__display_preferences WHERE id=1").fetchone()
|
||||
mode = conn.execute("SELECT message_loading_mode FROM u_prod__mail_preferences WHERE id=1").fetchone()
|
||||
require(values == (0,) and mode == ("pagination",), "SQLite V051 backfills defaults for existing V050 preference rows")
|
||||
conn.close()
|
||||
except Exception as exc:
|
||||
errors.append(f"SQLite V051 migration execution failed: {exc}")
|
||||
|
||||
pool = read("src/db/pool.rs")
|
||||
for dialect in ("SQLITE", "MYSQL", "PGSQL"):
|
||||
require(f"const {dialect}_MIGRATIONS: &[(u32, &str)]" in pool and "V050__compact_rows_and_signature_mode.sql" in pool, f"runtime migration catalog includes V050 ({dialect.lower()})")
|
||||
require("pub const BASE_SCHEMA_VERSION: u32 = 49" in pool, "consolidated squashed baseline is V049")
|
||||
require("V047__blob_keys_and_search_outbox.sql" not in pool and "V048__folder_counter_generation.sql" not in pool and "V049__blob_delete_outbox.sql" not in pool and "V051__adaptive_email_colors.sql" not in pool, "retired standalone migrations are absent from the runtime catalog")
|
||||
for dialect, rel in (
|
||||
("SQLITE", "sqlite"),
|
||||
("MYSQL", "mysql"),
|
||||
("PGSQL", "pgsql"),
|
||||
):
|
||||
expected = f'const {dialect}_MIGRATIONS: &[(u32, &str)] = &[(51, include_str!("../../migrations/{rel}/{active_migration}"))];'
|
||||
require(expected in pool, f"runtime migration catalog registers V051 for {rel}")
|
||||
require("pub const BASE_SCHEMA_VERSION: u32 = 50" in pool, "consolidated compatibility baseline remains V050")
|
||||
require("V050__compact_rows_and_signature_mode.sql" not in pool, "standalone V050 migration is removed from the runtime catalog")
|
||||
require(active_migration in pool, "V051 preference migration is registered in the runtime catalog")
|
||||
require("fn sqlite_v051_migrates_existing_v050_preferences()" in pool, "Rust unit test covers V050 to V051 SQLite migration")
|
||||
require("latest_schema_version" in pool, "runtime schema version is computed from migrations")
|
||||
require(
|
||||
"latest_supported_sqlite = SQLITE_MIGRATIONS" in pool
|
||||
and "if version > latest_supported_sqlite" in pool,
|
||||
"legacy SQLite import accepts the V049 baseline, applies V050 and rejects schemas newer than this build",
|
||||
"legacy SQLite import accepts V050-V051 and rejects schemas newer than this build",
|
||||
)
|
||||
|
||||
# Tenant/import table parity and transaction-bound mutation helpers.
|
||||
|
||||
Reference in new issue
Block a user