1 parent
95c0587c1e
commit
5b22add1f3
21 files changed
+293
-89
No files matched your search
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
./scripts/build-release.sh
|
||||
|
||||
VERSION="$(python3 - <<'PY'
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
with Path('Cargo.toml').open('rb') as fh:
|
||||
print(tomllib.load(fh)['package']['version'])
|
||||
PY
|
||||
)"
|
||||
|
||||
OUT_DIR="${1:-$ROOT/dist}"
|
||||
mkdir -p "$OUT_DIR"
|
||||
ARCHIVE="$OUT_DIR/RustBox-$VERSION.zip"
|
||||
CHECKSUM="$ARCHIVE.sha256"
|
||||
|
||||
python3 make_zip.py "$ARCHIVE"
|
||||
(
|
||||
cd "$(dirname "$ARCHIVE")"
|
||||
sha256sum "$(basename "$ARCHIVE")" > "$(basename "$CHECKSUM")"
|
||||
)
|
||||
|
||||
printf 'Release archive: %s\n' "$ARCHIVE"
|
||||
printf 'SHA256: '
|
||||
cat "$CHECKSUM"
|
||||
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user