mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-12 05:10:52 +02:00
prep for release
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { copyFileSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const require = createRequire(import.meta.url);
|
||||
const PACKAGE_ID = 'io.github.boof2015.astra';
|
||||
const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
|
||||
|
||||
const DISTRIBUTIONS = {
|
||||
github: {
|
||||
artifactSuffix: 'GitHub-arm-universal',
|
||||
extension: 'apk',
|
||||
label: 'GitHub',
|
||||
},
|
||||
'google-play': {
|
||||
artifactSuffix: 'GooglePlay',
|
||||
extension: 'aab',
|
||||
label: 'Google Play',
|
||||
},
|
||||
};
|
||||
|
||||
function readJson(relativePath) {
|
||||
return JSON.parse(readFileSync(path.join(ROOT, relativePath), 'utf8'));
|
||||
}
|
||||
|
||||
export function getReleaseIdentity(distribution) {
|
||||
const distributionConfig = DISTRIBUTIONS[distribution];
|
||||
if (!distributionConfig) {
|
||||
throw new Error(`Unsupported distribution ${JSON.stringify(distribution)}.`);
|
||||
}
|
||||
|
||||
const packageJson = readJson('package.json');
|
||||
const packageLock = readJson('package-lock.json');
|
||||
const release = readJson('release.json');
|
||||
const appJson = readJson('app.json');
|
||||
const versionName = packageJson.version;
|
||||
const versionCode = release.androidVersionCode;
|
||||
|
||||
if (typeof versionName !== 'string' || !SEMVER_PATTERN.test(versionName)) {
|
||||
throw new Error(`package.json version must be valid SemVer; received ${JSON.stringify(versionName)}.`);
|
||||
}
|
||||
if (packageLock.version !== versionName || packageLock.packages?.['']?.version !== versionName) {
|
||||
throw new Error('package-lock.json root versions must match package.json version. Run npm install after changing the version.');
|
||||
}
|
||||
if (!Number.isInteger(versionCode) || versionCode <= 0) {
|
||||
throw new Error('release.json androidVersionCode must be a positive integer.');
|
||||
}
|
||||
if (appJson.expo?.version !== undefined) {
|
||||
throw new Error('app.json must not duplicate the app version; app.config.js reads it from package.json.');
|
||||
}
|
||||
if (appJson.expo?.android?.package !== PACKAGE_ID) {
|
||||
throw new Error(`Android package ID must remain ${PACKAGE_ID}.`);
|
||||
}
|
||||
if (!appJson.expo?.plugins?.includes('./plugins/withAstraAndroidRelease')) {
|
||||
throw new Error('The Astra Android release config plugin is missing from app.json.');
|
||||
}
|
||||
|
||||
const previousDistribution = process.env.ASTRA_DISTRIBUTION;
|
||||
process.env.ASTRA_DISTRIBUTION = distribution;
|
||||
let resolvedConfig;
|
||||
try {
|
||||
const createAppConfig = require(path.join(ROOT, 'app.config.js'));
|
||||
resolvedConfig = createAppConfig({ config: appJson.expo });
|
||||
} finally {
|
||||
if (previousDistribution === undefined) {
|
||||
delete process.env.ASTRA_DISTRIBUTION;
|
||||
} else {
|
||||
process.env.ASTRA_DISTRIBUTION = previousDistribution;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
resolvedConfig.version !== versionName ||
|
||||
resolvedConfig.android?.versionCode !== versionCode ||
|
||||
resolvedConfig.extra?.distribution !== distribution
|
||||
) {
|
||||
throw new Error('Resolved Expo version, version code, or distribution does not match the tracked release identity.');
|
||||
}
|
||||
|
||||
return {
|
||||
artifactFileName: `Astra-${versionName}-${versionCode}-${distributionConfig.artifactSuffix}.${distributionConfig.extension}`,
|
||||
distribution,
|
||||
distributionLabel: distributionConfig.label,
|
||||
packageId: PACKAGE_ID,
|
||||
versionCode,
|
||||
versionName,
|
||||
};
|
||||
}
|
||||
|
||||
export function prepareArtifact(distribution, sourcePath, outputDirectory) {
|
||||
const identity = getReleaseIdentity(distribution);
|
||||
const absoluteSource = path.resolve(ROOT, sourcePath);
|
||||
const absoluteOutput = path.resolve(ROOT, outputDirectory);
|
||||
const artifactPath = path.join(absoluteOutput, identity.artifactFileName);
|
||||
|
||||
mkdirSync(absoluteOutput, { recursive: true });
|
||||
copyFileSync(absoluteSource, artifactPath);
|
||||
|
||||
const artifactBytes = readFileSync(artifactPath);
|
||||
const sha256 = createHash('sha256').update(artifactBytes).digest('hex');
|
||||
writeFileSync(`${artifactPath}.sha256`, `${sha256} ${identity.artifactFileName}\n`, 'utf8');
|
||||
|
||||
const metadata = {
|
||||
schemaVersion: 1,
|
||||
...identity,
|
||||
source: {
|
||||
commit: process.env.GITHUB_SHA ?? null,
|
||||
repository: process.env.GITHUB_REPOSITORY ?? null,
|
||||
runAttempt: process.env.GITHUB_RUN_ATTEMPT ?? null,
|
||||
runId: process.env.GITHUB_RUN_ID ?? null,
|
||||
},
|
||||
artifact: {
|
||||
fileName: identity.artifactFileName,
|
||||
sha256,
|
||||
sizeBytes: statSync(artifactPath).size,
|
||||
},
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
writeFileSync(path.join(absoluteOutput, 'build-metadata.json'), `${JSON.stringify(metadata, null, 2)}\n`, 'utf8');
|
||||
|
||||
return { artifactPath, metadata };
|
||||
}
|
||||
|
||||
export function verifyApkMetadata(distribution, metadataPath) {
|
||||
const identity = getReleaseIdentity(distribution);
|
||||
const metadata = readJson(path.relative(ROOT, path.resolve(ROOT, metadataPath)));
|
||||
const element = metadata.elements?.[0];
|
||||
|
||||
if (
|
||||
metadata.applicationId !== identity.packageId ||
|
||||
element?.versionCode !== identity.versionCode ||
|
||||
element?.versionName !== identity.versionName
|
||||
) {
|
||||
throw new Error(
|
||||
`APK metadata does not match ${identity.packageId} ${identity.versionName} (${identity.versionCode}).`
|
||||
);
|
||||
}
|
||||
|
||||
return identity;
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
console.error('Usage: node scripts/release/android-release.mjs validate <github|google-play>');
|
||||
console.error(' or: node scripts/release/android-release.mjs prepare <github|google-play> <source> <output-directory>');
|
||||
console.error(' or: node scripts/release/android-release.mjs verify-apk-metadata github <output-metadata.json>');
|
||||
}
|
||||
|
||||
function main() {
|
||||
const [command, distribution, sourcePath, outputDirectory] = process.argv.slice(2);
|
||||
if (command === 'validate' && distribution && !sourcePath && !outputDirectory) {
|
||||
const identity = getReleaseIdentity(distribution);
|
||||
console.log(`Validated ${identity.distributionLabel} ${identity.versionName} (${identity.versionCode}).`);
|
||||
return;
|
||||
}
|
||||
if (command === 'prepare' && distribution && sourcePath && outputDirectory) {
|
||||
const result = prepareArtifact(distribution, sourcePath, outputDirectory);
|
||||
console.log(`Prepared ${path.relative(ROOT, result.artifactPath)}.`);
|
||||
return;
|
||||
}
|
||||
if (command === 'verify-apk-metadata' && distribution === 'github' && sourcePath && !outputDirectory) {
|
||||
const identity = verifyApkMetadata(distribution, sourcePath);
|
||||
console.log(`Verified APK metadata for ${identity.versionName} (${identity.versionCode}).`);
|
||||
return;
|
||||
}
|
||||
|
||||
printUsage();
|
||||
process.exitCode = 2;
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
main();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { getReleaseIdentity } from './android-release.mjs';
|
||||
|
||||
test('builds stable artifact names from the tracked release identity', () => {
|
||||
assert.deepEqual(getReleaseIdentity('github'), {
|
||||
artifactFileName: 'Astra-0.1.0-1-GitHub-arm-universal.apk',
|
||||
distribution: 'github',
|
||||
distributionLabel: 'GitHub',
|
||||
packageId: 'io.github.boof2015.astra',
|
||||
versionCode: 1,
|
||||
versionName: '0.1.0',
|
||||
});
|
||||
assert.equal(getReleaseIdentity('google-play').artifactFileName, 'Astra-0.1.0-1-GooglePlay.aab');
|
||||
});
|
||||
|
||||
test('rejects unknown distribution channels', () => {
|
||||
assert.throws(() => getReleaseIdentity('nightly'), /Unsupported distribution/);
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
const TEST_SCRIPTS = [
|
||||
'test:release-config',
|
||||
'test:queue-actions',
|
||||
'test:desktop-remote',
|
||||
'test:dynamic-playlists',
|
||||
'test:album-grouping',
|
||||
'test:artist-grouping',
|
||||
'test:desktop-sync',
|
||||
'test:eq-share',
|
||||
'test:signal',
|
||||
'test:eq-math',
|
||||
'test:audio-startup',
|
||||
'test:seek-bar',
|
||||
'test:lyrics',
|
||||
'test:sleep',
|
||||
'test:troubleshooting',
|
||||
'test:settings-search',
|
||||
'test:now-playing-layout',
|
||||
'test:memory-lifecycle',
|
||||
'test:haptics',
|
||||
'test:home-greeting',
|
||||
'test:session',
|
||||
];
|
||||
|
||||
for (const script of TEST_SCRIPTS) {
|
||||
const result = spawnSync('npm', ['run', script], {
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32',
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user