Files
fressh/apps/mobile/scripts/signed-build.ts
EthanShoeDev 3f70c86bd4 fmt
2025-10-06 18:23:25 -04:00

180 lines
5.5 KiB
TypeScript

/**
* https://docs.expo.dev/guides/local-app-production/
*/
import * as fsp from 'fs/promises';
import * as path from 'path';
import { boolean, command, flag, oneOf, option, run } from 'cmd-ts';
import { z } from 'zod';
import packageJson from '../package.json' with { type: 'json' };
import { cmd } from './script-lib';
async function getSecrets(): Promise<{
keystoreBase64: string;
keystoreAlias: string;
keystorePassword: string;
}> {
const { stdout: rawBwItemString } = await cmd(
`bw get item "fressh keystore" --raw`,
{
stdio: 'pipe',
},
);
const bwItemSchema = z.looseObject({
login: z.looseObject({
username: z.string(),
password: z.string(),
}),
fields: z.array(
z.looseObject({
name: z.string(),
value: z.string(),
}),
),
});
const bwItem = bwItemSchema.parse(JSON.parse(rawBwItemString) as unknown, {
reportInput: true,
});
const keystoreBase64 = bwItem.fields.find(
(field) => field.name === 'keystore',
)?.value;
if (!keystoreBase64) throw new Error('Keystore not found');
return {
keystoreBase64,
keystoreAlias: bwItem.login.username,
keystorePassword: bwItem.login.password,
};
}
const signedBuildCommand = command({
name: 'signed-build',
description: 'Build a signed release build of the app',
args: {
format: option({
long: 'format',
type: oneOf(['aab', 'apk']),
short: 'f',
description: 'The format of the build to build',
defaultValue: () => 'aab',
}),
ghRelease: flag({
long: 'gh-release',
type: boolean,
short: 'g',
description:
'Whether to create a GitHub release (deprecated, use release-it instead)',
defaultValue: () => false,
}),
},
handler: async ({ format, ghRelease }) => {
{
if (ghRelease && format !== 'apk')
throw new Error('ghRelease is only supported for apk builds');
console.log(
'Making signed build. Format:',
format,
'GH Release:',
ghRelease,
);
const secrets = await getSecrets();
await cmd(`pnpm run prebuild:clean`);
// Ensure keystore is in the right place
// https://docs.expo.dev/guides/local-app-production/#create-an-upload-key
// Generated with:
// sudo keytool -genkey -v -keystore fressh-upload-key.keystore -alias fressh-key-alias -keyalg RSA -keysize 2048 -validity 10000
const keystorePath = `./android/app/fressh-upload-key.keystore`;
const keystoreFileName = path.basename(keystorePath);
// const bufferShouldEqual = await fsp.readFile(keystoreFileName, 'base64');
// await fsp.writeFile(
// './debug.log',
// JSON.stringify(
// {
// ...secrets,
// bufferShouldEqual,
// },
// null,
// 2,
// ),
// );
await fsp.writeFile(
keystorePath,
Buffer.from(secrets.keystoreBase64, 'base64'),
'base64',
);
console.log(`Keystore written to ${keystorePath}`);
// Ensure gradle.properties is configured
// https://docs.expo.dev/guides/local-app-production/#update-gradle-variables
const gradlePropertiesSuffix = `
FRESSH_UPLOAD_STORE_FILE=${keystoreFileName}
FRESSH_UPLOAD_KEY_ALIAS=${secrets.keystoreAlias}
FRESSH_UPLOAD_STORE_PASSWORD=${secrets.keystorePassword}
FRESSH_UPLOAD_KEY_PASSWORD=${secrets.keystorePassword}
`;
const currentGradleProperties = await fsp.readFile(
'./android/gradle.properties',
'utf8',
);
if (!currentGradleProperties.includes(gradlePropertiesSuffix.trim())) {
await fsp.writeFile(
'./android/gradle.properties',
`${currentGradleProperties}\n\n${gradlePropertiesSuffix}`,
);
console.log(`Gradle properties written to ./android/gradle.properties`);
}
// Ensure there is a release signing config in android/app/build.gradle
// https://docs.expo.dev/guides/local-app-production/#add-signing-config-to-buildgradle
const releaseSigningConfig = `
release {
if (project.hasProperty('FRESSH_UPLOAD_STORE_FILE')) {
storeFile file(FRESSH_UPLOAD_STORE_FILE)
storePassword FRESSH_UPLOAD_STORE_PASSWORD
keyAlias FRESSH_UPLOAD_KEY_ALIAS
keyPassword FRESSH_UPLOAD_KEY_PASSWORD
}
}`;
const currentBuildGradle = await fsp.readFile(
'./android/app/build.gradle',
'utf8',
);
if (!currentBuildGradle.includes(releaseSigningConfig.trim())) {
const newBuildGradle = currentBuildGradle
.replace(
/signingConfigs \{([\s\S]*?)\}/, // Modify existing signingConfigs without removing debug
(match) => {
if (match.includes('release {')) {
return match.replace(
/release \{([\s\S]*?)\}/,
releaseSigningConfig,
);
}
return match.trim() + releaseSigningConfig;
},
)
.replace(
/buildTypes \{([\s\S]*?)release \{([\s\S]*?)signingConfig signingConfigs\.debug/, // Ensure release config uses signingConfigs.release
`buildTypes { $1release { $2signingConfig signingConfigs.release`,
);
await fsp.writeFile('./android/app/build.gradle', newBuildGradle);
console.log(`Build gradle written to ./android/app/build.gradle`);
}
const bundleCommand =
format === 'aab' ? 'bundleRelease' : 'assembleRelease';
await cmd(`./gradlew app:${bundleCommand}`, {
relativeCwd: './android',
});
if (ghRelease)
await cmd(
`gh release create v${packageJson.version} ./android/app/build/outputs/apk/release/app-release.apk`,
);
}
},
});
void run(signedBuildCommand, process.argv.slice(2));