v0.2.2
RustBox CI / rustbox (push) Canceled after 0s

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-31 16:27:41 +01:00
1 parent 95c0587c1e
commit 5b22add1f3
21 files changed
+293 -89

No files matched your search

+90 -6
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Static/offline release checks for RustBox 0.2.1.
"""Static/offline release checks for RustBox 0.2.2.
These checks intentionally avoid pretending to replace `cargo test`: they validate
configuration, SQL migrations, Docker examples, query-catalog parity and release
@@ -49,19 +49,26 @@ 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.1", "Cargo package version is 0.2.1")
require(cargo["package"]["version"] == "0.2.2", "Cargo package version is 0.2.2")
require(cargo["package"]["rust-version"] == "1.88", "Cargo rust-version is 1.88")
toolchain = tomllib.loads(read("rust-toolchain.toml"))
require(toolchain["toolchain"]["channel"] == "1.88.0", "rust-toolchain pins Rust 1.88.0")
require(not (ROOT / "Cargo.lock").exists(), "stale Cargo.lock is not shipped")
require("cargo generate-lockfile" in read("scripts/build-release.sh"), "release script regenerates Cargo.lock")
release_script = read("scripts/build-release.sh")
require("cargo test --locked" in release_script, "release script requires the full Rust test suite")
require("cargo clippy --locked --all-targets -- -D warnings" in release_script, "release script requires warning-free Clippy")
require("cargo build --release --locked" in release_script, "release script requires a locked release build")
package_script = read("scripts/package-release.sh")
require("./scripts/build-release.sh" in package_script, "release packaging runs the full build gate before creating artifacts")
require("make_zip.py" in package_script and "sha256sum" in package_script, "release packaging creates ZIP and SHA256 only after the build gate")
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.1"), "README identifies RustBox 0.2.1")
require(read("RELEASE-MANIFEST.txt").startswith("RustBox 0.2.1"), "release manifest identifies RustBox 0.2.1")
require(read("RELEASE-VALIDATION.txt").startswith("RustBox 0.2.1"), "release validation identifies RustBox 0.2.1")
require(read("docs/IMPLEMENTATION_STATUS.md").startswith("# RustBox 0.2.1"), "implementation status identifies RustBox 0.2.1")
require(read("README.md").startswith("# RustBox 0.2.2"), "README identifies RustBox 0.2.2")
require(read("RELEASE-MANIFEST.txt").startswith("RustBox 0.2.2"), "release manifest identifies RustBox 0.2.2")
require(read("RELEASE-VALIDATION.txt").startswith("RustBox 0.2.2"), "release validation identifies RustBox 0.2.2")
require(read("docs/IMPLEMENTATION_STATUS.md").startswith("# RustBox 0.2.2"), "implementation status identifies RustBox 0.2.2")
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")
require(not re.search(r"(?m)^IMAP_HOST=\s*$", read(".env.example")), "environment example does not set an invalid empty IMAP host")
@@ -70,6 +77,78 @@ require("setOpenPgpAvailability" in read("src/web/js/72-openpgp-identities.js"),
require("is_opensearch_backpressure_error" in read("src/search/engine.rs"), "OpenSearch worker classifies backpressure failures")
require("worker pass failed; backing off" in read("src/search/engine.rs"), "OpenSearch worker backs off after failures")
require("ENVIRONMENT: ${ENVIRONMENT:-development}" in read("docker/compose-postgres-s3-opensearch.yml"), "PostgreSQL Compose defaults direct HTTP to development cookie mode")
session_db = read("src/session_store/database.rs")
db_pool = read("src/db/pool.rs")
require("crate::db::pool::configure(config).await?" in session_db, "database session store delegates schema initialization to db::pool::configure")
require("global_schema_for_backend" not in session_db, "database session store does not run a second backend schema pass")
require("sqlx::raw_sql(&schema)" not in session_db and "execute_batch(&schema)" not in session_db, "session store avoids duplicate schema DDL for SQLite/MySQL/PostgreSQL")
require("SET LOCAL client_min_messages TO WARNING" in db_pool, "PostgreSQL idempotent startup DDL suppresses expected NOTICE noise locally")
require("Executor as _" in db_pool, "PostgreSQL startup imports SQLx Executor for direct raw DDL execution")
require(".execute(schema.as_str())" in db_pool, "PostgreSQL startup executes schema DDL through Executor instead of raw_sql")
require(not re.search(r"raw_sql\(&schema\)\s*\.execute\(&mut \*tx\)", db_pool), "PostgreSQL startup avoids raw_sql mutable-connection lifetime regression")
require("sqlite_test_configuration_does_not_pin_deleted_tempdir" in db_pool, "SQLite test isolation has a regression test for deleted TempDir reuse")
require("let config = AppConfig {" in db_pool and "let mut config = AppConfig::default();" not in db_pool, "SQLite isolation regression test is Clippy-clean with struct initialization")
require("each test's own data_dir" in db_pool and "#[cfg(test)]" in db_pool, "SQLite test configuration does not publish a temporary database path globally")
require("RUST_LOG: ${RUST_LOG:-info,sqlx::postgres::notice=warn}" not in read("docker/compose-postgres-s3-opensearch.yml"), "PostgreSQL Compose no longer depends on a notice-filter workaround")
pgsql_queries = read("src/queries/pgsql/queries.rs")
require("ON CONFLICT(key) DO UPDATE SET value = CAST(user_meta.value AS INTEGER) + 1" in pgsql_queries, "PostgreSQL header-sync UPSERT qualifies the existing target value")
require("tenant_rewriter_prefixes_header_sync_target_qualifier" in db_pool, "tenant SQL rewriter has a regression test for qualified header-sync value")
mysql_queries = read("src/queries/mysql/queries.rs")
require("ON DUPLICATE KEY UPDATE value = CAST(value AS SIGNED) + 1" in mysql_queries, "MySQL header-sync uses its non-EXCLUDED duplicate-key form")
# PostgreSQL UPSERT audit: any read of an INSERT target column inside
# ON CONFLICT ... DO UPDATE must be qualified as either excluded.<column>
# or <target_table>.<column>. PostgreSQL otherwise treats names such as
# `value` as ambiguous between the existing target row and EXCLUDED.
def postgres_upsert_ambiguities(source: str) -> list[str]:
const_re = re.compile(
r'pub const\s+(\w+)\s*:\s*&str\s*=\s*(r#".*?"#|"(?:\\.|[^"\\])*")\s*;',
re.S,
)
problems: list[str] = []
for const_name, literal in const_re.findall(source):
if literal.startswith('r#"'):
sql = literal[3:-2]
else:
sql = bytes(literal[1:-1], 'utf-8').decode('unicode_escape')
if not re.search(r'ON\s+CONFLICT.*DO\s+UPDATE\s+SET', sql, re.I | re.S):
continue
insert = re.search(
r'INSERT\s+INTO\s+([A-Za-z_][\w]*)\s*\((.*?)\)\s*VALUES',
sql,
re.I | re.S,
)
if insert is None:
problems.append(f'{const_name}: unable to parse INSERT target')
continue
table = insert.group(1)
columns = [column.strip().strip('"') for column in insert.group(2).split(',')]
update = re.split(r'DO\s+UPDATE\s+SET', sql, maxsplit=1, flags=re.I)[1]
scan = re.sub(r"'(?:''|[^'])*'", '', update)
for column in columns:
candidate = re.sub(
rf'\b(?:excluded|{re.escape(table)})\s*\.\s*{re.escape(column)}\b',
'',
scan,
flags=re.I,
)
candidate = re.sub(
rf'(?:(?<=^)|(?<=,))\s*{re.escape(column)}\s*=',
'=',
candidate,
flags=re.I,
)
if re.search(rf'(?<!\.)\b{re.escape(column)}\b', candidate, re.I):
problems.append(f'{const_name}: unqualified target read `{column}` in {table} UPSERT')
return problems
pgsql_upsert_ambiguities = postgres_upsert_ambiguities(pgsql_queries)
require(
not pgsql_upsert_ambiguities,
'PostgreSQL ON CONFLICT target-column reads are fully qualified'
+ (f" ({'; '.join(pgsql_upsert_ambiguities)})" if pgsql_upsert_ambiguities else ''),
)
# Parse all maintained TOML examples.
toml_files = [
@@ -152,6 +231,11 @@ try:
require(outbox is not None, "fresh SQLite schema has search_outbox")
outbox_columns = {row[1] for row in conn.execute("PRAGMA table_info(u_test__search_outbox)")}
require("claimed_until" in outbox_columns, "fresh SQLite search_outbox separates worker lease from retry availability")
sqlite_header_sync = "INSERT INTO u_test__user_meta (key, value) VALUES ('header_sync_generation', '1') ON CONFLICT(key) DO UPDATE SET value = CAST(value AS INTEGER) + 1"
conn.execute(sqlite_header_sync)
conn.execute(sqlite_header_sync)
sqlite_generation = conn.execute("SELECT value FROM u_test__user_meta WHERE key='header_sync_generation'").fetchone()
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("'\"") == "46", "fresh global compatibility baseline remains V046 before tenant registration")