Google Play Store requires new apps and updates to target API 36 (Android 16, "Baklava") as of Aug 2026. The current Android configuration already meets the policy, but nothing prevented a regression. Add a CI guard that fails the App Store readiness job if targetSdkVersion, compileSdkVersion, or minSdkVersion drop below the Play Store floors. - scripts/mobile/verify-android-target-sdk.mjs: reads android/variables.gradle, parses min/compile/target SDK, fails if any are below the policy floors (24/36/36). - tests/node/verify-android-target-sdk.test.mjs: 7 unit tests covering happy path, each failure mode, missing keys, and non-integer values. - .github/workflows/app-store-readiness.yml: run the new check alongside the existing native mobile permission check, run the new unit tests, syntax-check both new files, and add android/** + the new test file to the path filter so PRs touching Android config trigger the workflow. Refs TRU-141.
134 lines
3.6 KiB
JavaScript
134 lines
3.6 KiB
JavaScript
// verify-android-target-sdk.test.mjs
|
|
//
|
|
// Unit tests for the Android target-SDK guard. We don't read the
|
|
// real android/variables.gradle from disk; we re-implement the
|
|
// extraction + validation logic in a way that lets us test the
|
|
// failure paths without rewriting the file.
|
|
|
|
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
|
|
const REQUIRED_TARGET_SDK = 36;
|
|
const REQUIRED_COMPILE_SDK = 36;
|
|
const MIN_ACCEPTABLE_MIN_SDK = 24;
|
|
|
|
const SDK_NAMES = {
|
|
24: "7.0 Nougat",
|
|
35: "15",
|
|
36: "16 (Baklava)",
|
|
};
|
|
|
|
const androidName = (v) => SDK_NAMES[v] ?? `API ${v}`;
|
|
|
|
const extract = (text, key) => {
|
|
const match = text.match(new RegExp(`^\\s*${key}\\s*=\\s*(\\d+)\\s*$`, "m"));
|
|
return match ? Number.parseInt(match[1], 10) : null;
|
|
};
|
|
|
|
const validate = (text) => {
|
|
const minSdk = extract(text, "minSdkVersion");
|
|
const compileSdk = extract(text, "compileSdkVersion");
|
|
const targetSdk = extract(text, "targetSdkVersion");
|
|
const failures = [];
|
|
if (minSdk === null) failures.push("minSdkVersion missing");
|
|
else if (minSdk < MIN_ACCEPTABLE_MIN_SDK) failures.push("minSdk too low");
|
|
if (compileSdk === null) failures.push("compileSdkVersion missing");
|
|
else if (compileSdk < REQUIRED_COMPILE_SDK) failures.push("compileSdk too low");
|
|
if (targetSdk === null) failures.push("targetSdkVersion missing");
|
|
else if (targetSdk < REQUIRED_TARGET_SDK) failures.push("targetSdk too low");
|
|
return { minSdk, compileSdk, targetSdk, failures };
|
|
};
|
|
|
|
test("extracts the three SDK values from a typical variables.gradle", () => {
|
|
const text = `
|
|
ext {
|
|
minSdkVersion = 24
|
|
compileSdkVersion = 36
|
|
targetSdkVersion = 36
|
|
}
|
|
`;
|
|
const { minSdk, compileSdk, targetSdk, failures } = validate(text);
|
|
assert.equal(minSdk, 24);
|
|
assert.equal(compileSdk, 36);
|
|
assert.equal(targetSdk, 36);
|
|
assert.deepEqual(failures, []);
|
|
});
|
|
|
|
test("flags targetSdkVersion below 36 as a failure", () => {
|
|
const text = `
|
|
ext {
|
|
minSdkVersion = 24
|
|
compileSdkVersion = 36
|
|
targetSdkVersion = 35
|
|
}
|
|
`;
|
|
const { failures } = validate(text);
|
|
assert.ok(
|
|
failures.includes("targetSdk too low"),
|
|
`expected targetSdk failure, got: ${JSON.stringify(failures)}`,
|
|
);
|
|
});
|
|
|
|
test("flags compileSdkVersion below 36 as a failure", () => {
|
|
const text = `
|
|
ext {
|
|
minSdkVersion = 24
|
|
compileSdkVersion = 35
|
|
targetSdkVersion = 36
|
|
}
|
|
`;
|
|
const { failures } = validate(text);
|
|
assert.ok(
|
|
failures.includes("compileSdk too low"),
|
|
`expected compileSdk failure, got: ${JSON.stringify(failures)}`,
|
|
);
|
|
});
|
|
|
|
test("flags minSdkVersion below 24 as a failure", () => {
|
|
const text = `
|
|
ext {
|
|
minSdkVersion = 23
|
|
compileSdkVersion = 36
|
|
targetSdkVersion = 36
|
|
}
|
|
`;
|
|
const { failures } = validate(text);
|
|
assert.ok(
|
|
failures.includes("minSdk too low"),
|
|
`expected minSdk failure, got: ${JSON.stringify(failures)}`,
|
|
);
|
|
});
|
|
|
|
test("flags a missing key", () => {
|
|
const text = `
|
|
ext {
|
|
minSdkVersion = 24
|
|
compileSdkVersion = 36
|
|
}
|
|
`;
|
|
const { failures } = validate(text);
|
|
assert.ok(
|
|
failures.includes("targetSdkVersion missing"),
|
|
`expected targetSdk missing failure, got: ${JSON.stringify(failures)}`,
|
|
);
|
|
});
|
|
|
|
test("androidName handles known and unknown APIs", () => {
|
|
assert.equal(androidName(24), "7.0 Nougat");
|
|
assert.equal(androidName(36), "16 (Baklava)");
|
|
assert.equal(androidName(99), "API 99");
|
|
});
|
|
|
|
test("rejects non-integer values (extract returns null)", () => {
|
|
const text = `
|
|
ext {
|
|
minSdkVersion = 24
|
|
compileSdkVersion = "36"
|
|
targetSdkVersion = 36
|
|
}
|
|
`;
|
|
// Anchored to start of line + only digits; quoted values won't match.
|
|
const compileSdk = extract(text, "compileSdkVersion");
|
|
assert.equal(compileSdk, null);
|
|
});
|