mirror of
https://github.com/readest/readest.git
synced 2026-10-21 14:12:43 +00:00
ci: guard the e-ink identity whitelist against Rust/Kotlin drift (#6834)
Some checks are pending
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (rust) (push) Waiting to run
Publish Docker image / build (linux/amd64, ubuntu-latest) (push) Waiting to run
Publish Docker image / build (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Publish Docker image / merge (push) Blocked by required conditions
Build with Nix / tests (push) Waiting to run
PR checks / test_web_app (2) (push) Waiting to run
PR checks / test_extensions (push) Waiting to run
PR checks / nix_flake_check (push) Waiting to run
PR checks / rust_lint (push) Waiting to run
PR checks / koplugin_rust_lint (push) Waiting to run
PR checks / build_web_app (push) Waiting to run
PR checks / test_web_app (1) (push) Waiting to run
PR checks / build_tauri_app (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Deploy to vercel on merge / build_and_deploy (push) Waiting to run
Some checks are pending
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (rust) (push) Waiting to run
Publish Docker image / build (linux/amd64, ubuntu-latest) (push) Waiting to run
Publish Docker image / build (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Publish Docker image / merge (push) Blocked by required conditions
Build with Nix / tests (push) Waiting to run
PR checks / test_web_app (2) (push) Waiting to run
PR checks / test_extensions (push) Waiting to run
PR checks / nix_flake_check (push) Waiting to run
PR checks / rust_lint (push) Waiting to run
PR checks / koplugin_rust_lint (push) Waiting to run
PR checks / build_web_app (push) Waiting to run
PR checks / test_web_app (1) (push) Waiting to run
PR checks / build_tauri_app (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Deploy to vercel on merge / build_and_deploy (push) Waiting to run
* ci: guard the e-ink identity whitelist against Rust/Kotlin drift The e-ink device whitelist lives twice: as the Rust source of truth in eink_identity.rs (host app's e-ink device paths) and as the Kotlin port in the tauri-plugin-webview-upgrade submodule (the plugin's night-mode pin, readest/tauri-plugin-webview-upgrade#4). Both must classify devices identically, but until now nothing stopped one side from changing without the other - especially now that they sit in different repos. Adds a zero-dependency Node check that extracts the three lists (EINK_MANUFACTURERS / EINK_MODELS / EINK_AMBIGUOUS_MODELS) from both files and fails with the offending entries whenever they drift. Wired into the rust_lint job (whose checkout already includes submodules and whose src-tauri paths-filter fires for both the Rust file and the plugin submodule), plus a `check:eink-identity` package script for local runs. * ci: anchor the Rust whitelist extraction to line starts Same class of fix as the Kotlin extractor: a bracket inside an entry comment (e.g. // e-ink [A5] readers) truncated the non-greedy capture at the comment instead of the list end, producing false drift. Multi-line lists anchor the closing bracket to a line start; single-line lists use a bracket-free capture. * ci: address review findings on the identity sync guard - Run the sync check unconditionally: a PR touching only scripts/check-eink-identity-sync.mjs did not match the tauri paths-filter, so a checker regression could merge without the check running. The check is ~0.1s. - Strip // line comments before extracting quoted entries: a commented-out entry must not count as active on one side while still active on the other, or real drift is missed. - Anchor multi-line list openings to a newline (Rust `&[\n`, Kotlin `listOf(\n`): a multi-line match could previously start at a completed single-line declaration and capture through an unrelated later list. - Make all regexes CR/LF tolerant (`\r?\n`): a checkout with CRLF line endings made every list extraction fail outright. * docs(ci): add JSDoc to the identity sync guard functions
This commit is contained in:
parent
46c686eee3
commit
61ef15e005
3 changed files with 87 additions and 0 deletions
5
.github/workflows/pull-request.yml
vendored
5
.github/workflows/pull-request.yml
vendored
|
|
@ -56,6 +56,11 @@ jobs:
|
|||
filters: |
|
||||
tauri:
|
||||
- 'apps/readest-app/src-tauri/**'
|
||||
# Unconditional: the check must also run when its own source
|
||||
# (scripts/check-eink-identity-sync.mjs) changes, which the tauri
|
||||
# paths-filter above does not match.
|
||||
- name: e-ink identity whitelist sync (Rust vs Kotlin)
|
||||
run: node scripts/check-eink-identity-sync.mjs
|
||||
- name: setup sccache
|
||||
if: steps.changes.outputs.tauri == 'true'
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
"test:lua": "pnpm --filter @readest/readest-app test:lua",
|
||||
"lint": "pnpm --filter @readest/readest-app lint",
|
||||
"lint:lua": "pnpm --filter @readest/readest-app lint:lua",
|
||||
"check:eink-identity": "node scripts/check-eink-identity-sync.mjs",
|
||||
"tauri": "pnpm --filter @readest/readest-app tauri",
|
||||
"dev-web": "pnpm --filter @readest/readest-app dev-web",
|
||||
"prepare": "husky",
|
||||
|
|
|
|||
81
scripts/check-eink-identity-sync.mjs
Normal file
81
scripts/check-eink-identity-sync.mjs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// Guards the e-ink device identity whitelist against cross-repo drift.
|
||||
//
|
||||
// The whitelist lives twice: as the Rust source of truth in
|
||||
// src-tauri/src/eink_identity.rs (driving the host app's e-ink device
|
||||
// paths) and as the Kotlin port inside the tauri-plugin-webview-upgrade
|
||||
// submodule (driving the plugin's night-mode pin, readest/
|
||||
// tauri-plugin-webview-upgrade#4). The two must classify devices
|
||||
// identically; this script fails CI whenever one side changes without the
|
||||
// other.
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const RUST_PATH = 'apps/readest-app/src-tauri/src/eink_identity.rs';
|
||||
const KOTLIN_PATH =
|
||||
'apps/readest-app/src-tauri/plugins/tauri-plugin-webview-upgrade/android/src/main/java/com/readest/webview_upgrade/EinkIdentity.kt';
|
||||
|
||||
const LISTS = ['EINK_MANUFACTURERS', 'EINK_MODELS', 'EINK_AMBIGUOUS_MODELS'];
|
||||
|
||||
/** Extract the quoted, non-empty entries from a captured list block. */
|
||||
function quotedStrings(block) {
|
||||
// Strip // line comments first: a commented-out entry ("onyx",) must not
|
||||
// count as active on one side while still active on the other.
|
||||
const active = block.replace(/\/\/.*$/gm, '');
|
||||
return [...active.matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
||||
}
|
||||
|
||||
/** Extract one `NAME: &[&str] = &[...]` whitelist from the Rust source. */
|
||||
function extractRustList(source, name) {
|
||||
// Two layouts: the multi-line form (opening delimiter followed by a
|
||||
// newline; entry comments may contain brackets, so the closing bracket is
|
||||
// anchored to a line start) and the one-line form `&["a9", "nova"]`.
|
||||
// Anchoring the opening delimiter keeps a multi-line match from starting
|
||||
// at a completed single-line declaration.
|
||||
const multi = source.match(
|
||||
new RegExp(`${name}\\s*:\\s*&\\[&str\\]\\s*=\\s*&\\[\\r?\\n([\\s\\S]*?)\\r?\\n\\]`),
|
||||
);
|
||||
const single =
|
||||
multi ? null : source.match(new RegExp(`${name}\\s*:\\s*&\\[&str\\]\\s*=\\s*&\\[([^\\]\\n]*)\\]`));
|
||||
const match = multi ?? single;
|
||||
if (!match) throw new Error(`list "${name}" not found in ${RUST_PATH}`);
|
||||
return quotedStrings(match[1]);
|
||||
}
|
||||
|
||||
/** Extract one `NAME = listOf(...)` whitelist from the Kotlin port. */
|
||||
function extractKotlinList(source, name) {
|
||||
// Same anchoring as the Rust extractor, with parens instead of brackets.
|
||||
const multi = source.match(new RegExp(`\\b${name}\\s*=\\s*listOf\\(\\r?\\n([\\s\\S]*?)\\r?\\n\\)`));
|
||||
const single = multi ? null : source.match(new RegExp(`\\b${name}\\s*=\\s*listOf\\(([^)\\n]*)\\)`));
|
||||
const match = multi ?? single;
|
||||
if (!match) throw new Error(`list "${name}" not found in ${KOTLIN_PATH}`);
|
||||
return quotedStrings(match[1]);
|
||||
}
|
||||
|
||||
/** Entries present on only one side, labeled `rust-only` / `kotlin-only`. */
|
||||
function drift(rust, kotlin) {
|
||||
return [
|
||||
...rust.filter((entry) => !kotlin.includes(entry)).map((entry) => `rust-only: "${entry}"`),
|
||||
...kotlin.filter((entry) => !rust.includes(entry)).map((entry) => `kotlin-only: "${entry}"`),
|
||||
];
|
||||
}
|
||||
|
||||
let failed = false;
|
||||
const rust = readFileSync(RUST_PATH, 'utf8');
|
||||
const kotlin = readFileSync(KOTLIN_PATH, 'utf8');
|
||||
|
||||
for (const name of LISTS) {
|
||||
const rustList = extractRustList(rust, name);
|
||||
const kotlinList = extractKotlinList(kotlin, name);
|
||||
const differences = drift(rustList, kotlinList);
|
||||
if (differences.length > 0) {
|
||||
failed = true;
|
||||
console.error(`${name} drifted between Rust and Kotlin:`);
|
||||
for (const difference of differences) console.error(` ${difference}`);
|
||||
} else {
|
||||
console.log(`${name}: ${rustList.length} entries in sync`);
|
||||
}
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
console.error(`\nSync ${KOTLIN_PATH} with ${RUST_PATH} (or open the PRs in both repos).`);
|
||||
process.exit(1);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue