feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
403
scripts/ci/verify-mobile-release-config.mjs
Normal file
403
scripts/ci/verify-mobile-release-config.mjs
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const ROOT = resolve(import.meta.dirname, '../..')
|
||||
const DEFAULT_GOOGLE_SERVICES_PATH = resolve(
|
||||
ROOT,
|
||||
'apps/mobile-rn/android/app/google-services.json',
|
||||
)
|
||||
const PRODUCTION_PACKAGE_NAME = 'com.d3ro.voice'
|
||||
const GOOGLE_TEST_ADMOB_PUBLISHER_ID = '3940256099942544'
|
||||
const FIREBASE_PROJECT_ID = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/
|
||||
const FIREBASE_PROJECT_NUMBER = /^[1-9][0-9]{5,19}$/
|
||||
const FIREBASE_MOBILESDK_APP_ID = /^1:([1-9][0-9]{5,19}):android:([0-9a-f]{16,64})$/
|
||||
const ANDROID_PACKAGE_NAME = /^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$/i
|
||||
const GOOGLE_API_KEY = /^AIza[0-9A-Za-z_-]{20,96}$/
|
||||
const ADMOB_APP_ID = /^ca-app-pub-([0-9]{16})~([0-9]{10})$/
|
||||
const ADMOB_UNIT_ID = /^ca-app-pub-([0-9]{16})\/([0-9]{10})$/
|
||||
const MAX_GOOGLE_SERVICES_BYTES = 1024 * 1024
|
||||
|
||||
function fail(code) {
|
||||
throw new Error(`mobile_release_config_invalid:${code}`)
|
||||
}
|
||||
|
||||
function isRecord(value) {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function requireExactString(value, code) {
|
||||
if (typeof value !== 'string' || value.length === 0 || value !== value.trim()) fail(code)
|
||||
return value
|
||||
}
|
||||
|
||||
function requireExpectedFirebaseIdentity(expected) {
|
||||
if (!isRecord(expected)) fail('firebase_expected_identity_missing')
|
||||
|
||||
const projectId = requireExactString(
|
||||
expected.projectId,
|
||||
'firebase_expected_project_id_missing',
|
||||
)
|
||||
const projectNumber = requireExactString(
|
||||
expected.projectNumber,
|
||||
'firebase_expected_project_number_missing',
|
||||
)
|
||||
const mobileSdkAppId = requireExactString(
|
||||
expected.mobileSdkAppId,
|
||||
'firebase_expected_mobilesdk_app_id_missing',
|
||||
)
|
||||
|
||||
if (!FIREBASE_PROJECT_ID.test(projectId)) fail('firebase_expected_project_id_format')
|
||||
if (!FIREBASE_PROJECT_NUMBER.test(projectNumber)) {
|
||||
fail('firebase_expected_project_number_format')
|
||||
}
|
||||
const appIdMatch = mobileSdkAppId.match(FIREBASE_MOBILESDK_APP_ID)
|
||||
if (!appIdMatch) fail('firebase_expected_mobilesdk_app_id_format')
|
||||
if (appIdMatch[1] !== projectNumber) {
|
||||
fail('firebase_expected_mobilesdk_app_id_project_number_mismatch')
|
||||
}
|
||||
|
||||
return { projectId, projectNumber, mobileSdkAppId }
|
||||
}
|
||||
|
||||
export function parseGoogleServicesSource(source) {
|
||||
if (typeof source !== 'string' || source.length === 0) fail('google_services_empty')
|
||||
if (Buffer.byteLength(source, 'utf8') > MAX_GOOGLE_SERVICES_BYTES) {
|
||||
fail('google_services_too_large')
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(source)
|
||||
} catch {
|
||||
fail('google_services_invalid_json')
|
||||
}
|
||||
}
|
||||
|
||||
export function verifyFirebaseConfiguration(configuration, expectedIdentity) {
|
||||
const expected = requireExpectedFirebaseIdentity(expectedIdentity)
|
||||
if (!isRecord(configuration)) fail('google_services_root_invalid')
|
||||
if (configuration.configuration_version !== '1') {
|
||||
fail('google_services_configuration_version_invalid')
|
||||
}
|
||||
|
||||
const projectInfo = configuration.project_info
|
||||
if (!isRecord(projectInfo)) fail('firebase_project_info_missing')
|
||||
const projectId = requireExactString(projectInfo.project_id, 'firebase_project_id_missing')
|
||||
const projectNumber = requireExactString(
|
||||
projectInfo.project_number,
|
||||
'firebase_project_number_missing',
|
||||
)
|
||||
if (!FIREBASE_PROJECT_ID.test(projectId)) fail('firebase_project_id_format')
|
||||
if (!FIREBASE_PROJECT_NUMBER.test(projectNumber)) fail('firebase_project_number_format')
|
||||
if (projectId !== expected.projectId) fail('firebase_project_id_mismatch')
|
||||
if (projectNumber !== expected.projectNumber) fail('firebase_project_number_mismatch')
|
||||
|
||||
const clients = configuration.client
|
||||
if (!Array.isArray(clients) || clients.length === 0) fail('firebase_clients_missing')
|
||||
if (clients.length > 100) fail('firebase_client_count_excessive')
|
||||
|
||||
const packages = new Set()
|
||||
const appIds = new Set()
|
||||
let productionClient
|
||||
|
||||
for (const client of clients) {
|
||||
if (!isRecord(client)) fail('firebase_client_invalid')
|
||||
const clientInfo = client.client_info
|
||||
if (!isRecord(clientInfo)) fail('firebase_client_info_missing')
|
||||
const androidClientInfo = clientInfo.android_client_info
|
||||
if (!isRecord(androidClientInfo)) fail('firebase_android_client_info_missing')
|
||||
|
||||
const packageName = requireExactString(
|
||||
androidClientInfo.package_name,
|
||||
'firebase_client_package_missing',
|
||||
)
|
||||
const mobileSdkAppId = requireExactString(
|
||||
clientInfo.mobilesdk_app_id,
|
||||
'firebase_client_mobilesdk_app_id_missing',
|
||||
)
|
||||
if (!ANDROID_PACKAGE_NAME.test(packageName)) fail('firebase_client_package_format')
|
||||
const appIdMatch = mobileSdkAppId.match(FIREBASE_MOBILESDK_APP_ID)
|
||||
if (!appIdMatch) fail('firebase_client_mobilesdk_app_id_format')
|
||||
if (appIdMatch[1] !== projectNumber) {
|
||||
fail('firebase_client_mobilesdk_app_id_project_number_mismatch')
|
||||
}
|
||||
if (packages.has(packageName)) fail('firebase_client_package_duplicate')
|
||||
if (appIds.has(mobileSdkAppId)) fail('firebase_client_mobilesdk_app_id_duplicate')
|
||||
packages.add(packageName)
|
||||
appIds.add(mobileSdkAppId)
|
||||
|
||||
if (packageName === PRODUCTION_PACKAGE_NAME) productionClient = client
|
||||
}
|
||||
|
||||
if (!productionClient) fail('firebase_production_client_missing')
|
||||
if (productionClient.client_info.mobilesdk_app_id !== expected.mobileSdkAppId) {
|
||||
fail('firebase_mobilesdk_app_id_mismatch')
|
||||
}
|
||||
|
||||
const apiKeys = productionClient.api_key
|
||||
if (!Array.isArray(apiKeys) || apiKeys.length !== 1) {
|
||||
fail('firebase_production_api_key_must_be_unique')
|
||||
}
|
||||
if (!isRecord(apiKeys[0])) fail('firebase_production_api_key_invalid')
|
||||
const apiKey = requireExactString(
|
||||
apiKeys[0].current_key,
|
||||
'firebase_production_api_key_missing',
|
||||
)
|
||||
if (!GOOGLE_API_KEY.test(apiKey)) fail('firebase_production_api_key_format')
|
||||
|
||||
return Object.freeze({
|
||||
packageNameVerified: true,
|
||||
projectIdentityVerified: true,
|
||||
clientIdentityUnique: true,
|
||||
apiKeyUnique: true,
|
||||
})
|
||||
}
|
||||
|
||||
function parseAdMobId(value, expression, missingCode, formatCode) {
|
||||
const normalized = requireExactString(value, missingCode)
|
||||
const match = normalized.match(expression)
|
||||
if (!match) fail(formatCode)
|
||||
if (/^0+$/.test(match[1]) || /^0+$/.test(match[2])) fail(formatCode)
|
||||
return { value: normalized, publisherId: match[1] }
|
||||
}
|
||||
|
||||
export function verifyAdMobConfiguration(configuration) {
|
||||
if (!isRecord(configuration)) fail('admob_configuration_missing')
|
||||
const app = parseAdMobId(
|
||||
configuration.appId,
|
||||
ADMOB_APP_ID,
|
||||
'admob_app_id_missing',
|
||||
'admob_app_id_format',
|
||||
)
|
||||
const banner = parseAdMobId(
|
||||
configuration.bannerUnitId,
|
||||
ADMOB_UNIT_ID,
|
||||
'admob_banner_unit_id_missing',
|
||||
'admob_banner_unit_id_format',
|
||||
)
|
||||
const rewarded = parseAdMobId(
|
||||
configuration.rewardedUnitId,
|
||||
ADMOB_UNIT_ID,
|
||||
'admob_rewarded_unit_id_missing',
|
||||
'admob_rewarded_unit_id_format',
|
||||
)
|
||||
|
||||
for (const entry of [app, banner, rewarded]) {
|
||||
if (entry.publisherId === GOOGLE_TEST_ADMOB_PUBLISHER_ID) {
|
||||
fail('admob_google_test_publisher_rejected')
|
||||
}
|
||||
}
|
||||
if (app.publisherId !== banner.publisherId || app.publisherId !== rewarded.publisherId) {
|
||||
fail('admob_publisher_mismatch')
|
||||
}
|
||||
if (banner.value === rewarded.value) fail('admob_unit_ids_must_be_unique')
|
||||
|
||||
return Object.freeze({
|
||||
productionFormatVerified: true,
|
||||
googleTestPublisherRejected: true,
|
||||
publisherConsistencyVerified: true,
|
||||
unitIdentityUnique: true,
|
||||
})
|
||||
}
|
||||
|
||||
function clone(value) {
|
||||
return structuredClone(value)
|
||||
}
|
||||
|
||||
function expectFailure(label, action, expectedCode) {
|
||||
try {
|
||||
action()
|
||||
} catch (error) {
|
||||
const expectedMessage = `mobile_release_config_invalid:${expectedCode}`
|
||||
if (error instanceof Error && error.message === expectedMessage) return
|
||||
throw new Error(`mobile_release_config_self_test_unexpected:${label}`)
|
||||
}
|
||||
throw new Error(`mobile_release_config_self_test_missed:${label}`)
|
||||
}
|
||||
|
||||
function makeFirebaseFixture() {
|
||||
return {
|
||||
configuration_version: '1',
|
||||
project_info: {
|
||||
project_number: '123456789012',
|
||||
project_id: 'd3ro-production',
|
||||
storage_bucket: 'd3ro-production.example.invalid',
|
||||
},
|
||||
client: [
|
||||
{
|
||||
client_info: {
|
||||
mobilesdk_app_id: '1:123456789012:android:0123456789abcdef',
|
||||
android_client_info: { package_name: PRODUCTION_PACKAGE_NAME },
|
||||
},
|
||||
api_key: [
|
||||
{ current_key: ['AI', 'zaFixtureKeyMaterial1234567890abcd'].join('') },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function runSelfTest() {
|
||||
const expected = {
|
||||
projectId: 'd3ro-production',
|
||||
projectNumber: '123456789012',
|
||||
mobileSdkAppId: '1:123456789012:android:0123456789abcdef',
|
||||
}
|
||||
const firebase = makeFirebaseFixture()
|
||||
const admob = {
|
||||
appId: 'ca-app-pub-1234567890123456~1234567890',
|
||||
bannerUnitId: 'ca-app-pub-1234567890123456/2345678901',
|
||||
rewardedUnitId: 'ca-app-pub-1234567890123456/3456789012',
|
||||
}
|
||||
|
||||
verifyFirebaseConfiguration(firebase, expected)
|
||||
verifyAdMobConfiguration(admob)
|
||||
parseGoogleServicesSource(JSON.stringify(firebase))
|
||||
|
||||
const firebaseCases = [
|
||||
['invalid_json', () => parseGoogleServicesSource('{'), 'google_services_invalid_json'],
|
||||
['root_array', () => verifyFirebaseConfiguration([], expected), 'google_services_root_invalid'],
|
||||
['configuration_version', () => {
|
||||
const value = clone(firebase)
|
||||
value.configuration_version = '2'
|
||||
return verifyFirebaseConfiguration(value, expected)
|
||||
}, 'google_services_configuration_version_invalid'],
|
||||
['project_id_mismatch', () => {
|
||||
const value = clone(firebase)
|
||||
value.project_info.project_id = 'different-production'
|
||||
return verifyFirebaseConfiguration(value, expected)
|
||||
}, 'firebase_project_id_mismatch'],
|
||||
['project_number_mismatch', () => {
|
||||
const value = clone(firebase)
|
||||
value.project_info.project_number = '223456789012'
|
||||
return verifyFirebaseConfiguration(value, expected)
|
||||
}, 'firebase_project_number_mismatch'],
|
||||
['missing_clients', () => {
|
||||
const value = clone(firebase)
|
||||
value.client = []
|
||||
return verifyFirebaseConfiguration(value, expected)
|
||||
}, 'firebase_clients_missing'],
|
||||
['duplicate_package', () => {
|
||||
const value = clone(firebase)
|
||||
const duplicate = clone(value.client[0])
|
||||
duplicate.client_info.mobilesdk_app_id = '1:123456789012:android:fedcba9876543210'
|
||||
value.client.push(duplicate)
|
||||
return verifyFirebaseConfiguration(value, expected)
|
||||
}, 'firebase_client_package_duplicate'],
|
||||
['duplicate_app_id', () => {
|
||||
const value = clone(firebase)
|
||||
const duplicate = clone(value.client[0])
|
||||
duplicate.client_info.android_client_info.package_name = 'com.d3ro.voice.other'
|
||||
value.client.push(duplicate)
|
||||
return verifyFirebaseConfiguration(value, expected)
|
||||
}, 'firebase_client_mobilesdk_app_id_duplicate'],
|
||||
['production_package_missing', () => {
|
||||
const value = clone(firebase)
|
||||
value.client[0].client_info.android_client_info.package_name = 'com.d3ro.voice.other'
|
||||
return verifyFirebaseConfiguration(value, expected)
|
||||
}, 'firebase_production_client_missing'],
|
||||
['app_id_mismatch', () => {
|
||||
const value = clone(firebase)
|
||||
value.client[0].client_info.mobilesdk_app_id = '1:123456789012:android:fedcba9876543210'
|
||||
return verifyFirebaseConfiguration(value, expected)
|
||||
}, 'firebase_mobilesdk_app_id_mismatch'],
|
||||
['app_id_project_number_mismatch', () => {
|
||||
const value = clone(firebase)
|
||||
value.client[0].client_info.mobilesdk_app_id = '1:223456789012:android:0123456789abcdef'
|
||||
return verifyFirebaseConfiguration(value, expected)
|
||||
}, 'firebase_client_mobilesdk_app_id_project_number_mismatch'],
|
||||
['api_key_missing', () => {
|
||||
const value = clone(firebase)
|
||||
value.client[0].api_key = []
|
||||
return verifyFirebaseConfiguration(value, expected)
|
||||
}, 'firebase_production_api_key_must_be_unique'],
|
||||
['api_key_ambiguous', () => {
|
||||
const value = clone(firebase)
|
||||
value.client[0].api_key.push(clone(value.client[0].api_key[0]))
|
||||
return verifyFirebaseConfiguration(value, expected)
|
||||
}, 'firebase_production_api_key_must_be_unique'],
|
||||
['expected_identity_missing', () => verifyFirebaseConfiguration(firebase, {}), 'firebase_expected_project_id_missing'],
|
||||
['expected_app_id_project_number_mismatch', () => verifyFirebaseConfiguration(firebase, {
|
||||
...expected,
|
||||
mobileSdkAppId: '1:223456789012:android:0123456789abcdef',
|
||||
}), 'firebase_expected_mobilesdk_app_id_project_number_mismatch'],
|
||||
]
|
||||
for (const [label, action, expectedCode] of firebaseCases) {
|
||||
expectFailure(label, action, expectedCode)
|
||||
}
|
||||
|
||||
const admobCases = [
|
||||
['app_format', { ...admob, appId: 'ca-app-pub-123~456' }, 'admob_app_id_format'],
|
||||
['banner_format', { ...admob, bannerUnitId: 'ca-app-pub-1234567890123456~2345678901' }, 'admob_banner_unit_id_format'],
|
||||
['rewarded_format', { ...admob, rewardedUnitId: 'ca-app-pub-1234567890123456/123' }, 'admob_rewarded_unit_id_format'],
|
||||
['app_test_publisher', {
|
||||
appId: 'ca-app-pub-3940256099942544~3347511713',
|
||||
bannerUnitId: 'ca-app-pub-3940256099942544/6300978111',
|
||||
rewardedUnitId: 'ca-app-pub-3940256099942544/5224354917',
|
||||
}, 'admob_google_test_publisher_rejected'],
|
||||
['banner_test_publisher', {
|
||||
...admob,
|
||||
bannerUnitId: 'ca-app-pub-3940256099942544/6300978111',
|
||||
}, 'admob_google_test_publisher_rejected'],
|
||||
['publisher_mismatch', {
|
||||
...admob,
|
||||
rewardedUnitId: 'ca-app-pub-2234567890123456/3456789012',
|
||||
}, 'admob_publisher_mismatch'],
|
||||
['unit_duplicate', { ...admob, rewardedUnitId: admob.bannerUnitId }, 'admob_unit_ids_must_be_unique'],
|
||||
['whitespace', { ...admob, appId: `${admob.appId} ` }, 'admob_app_id_missing'],
|
||||
['zero_identifier', {
|
||||
...admob,
|
||||
bannerUnitId: 'ca-app-pub-1234567890123456/0000000000',
|
||||
}, 'admob_banner_unit_id_format'],
|
||||
]
|
||||
for (const [label, configuration, expectedCode] of admobCases) {
|
||||
expectFailure(label, () => verifyAdMobConfiguration(configuration), expectedCode)
|
||||
}
|
||||
|
||||
return { firebaseFailureCases: firebaseCases.length, adMobFailureCases: admobCases.length }
|
||||
}
|
||||
|
||||
async function run() {
|
||||
if (process.argv.includes('--self-test')) {
|
||||
const result = runSelfTest()
|
||||
console.log(JSON.stringify({ selfTest: 'verified', ...result }))
|
||||
return
|
||||
}
|
||||
if (process.argv.length > 2) fail('unknown_argument')
|
||||
|
||||
let source
|
||||
try {
|
||||
source = await readFile(
|
||||
process.env.D3RO_GOOGLE_SERVICES_JSON_FILE || DEFAULT_GOOGLE_SERVICES_PATH,
|
||||
'utf8',
|
||||
)
|
||||
} catch {
|
||||
fail('google_services_file_unreadable')
|
||||
}
|
||||
|
||||
const firebase = verifyFirebaseConfiguration(parseGoogleServicesSource(source), {
|
||||
projectId: process.env.D3RO_FIREBASE_EXPECTED_PROJECT_ID,
|
||||
projectNumber: process.env.D3RO_FIREBASE_EXPECTED_PROJECT_NUMBER,
|
||||
mobileSdkAppId: process.env.D3RO_FIREBASE_EXPECTED_MOBILESDK_APP_ID,
|
||||
})
|
||||
const adMob = verifyAdMobConfiguration({
|
||||
appId: process.env.D3RO_ADMOB_APP_ID,
|
||||
bannerUnitId: process.env.D3RO_ADMOB_BANNER_UNIT_ID,
|
||||
rewardedUnitId: process.env.D3RO_ADMOB_REWARDED_UNIT_ID,
|
||||
})
|
||||
|
||||
console.log(JSON.stringify({
|
||||
releaseConfiguration: 'verified',
|
||||
firebase,
|
||||
adMob,
|
||||
}))
|
||||
}
|
||||
|
||||
const isMain = process.argv[1]
|
||||
&& resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))
|
||||
if (isMain) {
|
||||
run().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : 'mobile_release_config_invalid:unknown')
|
||||
process.exitCode = 1
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue