signed script

This commit is contained in:
EthanShoeDev
2025-09-10 02:53:13 -04:00
parent 19e0348978
commit f31d52bb00
4 changed files with 223 additions and 0 deletions

View File

@@ -8,6 +8,8 @@
"start": "expo start",
"android": "expo run:android",
"android:release": "expo run:android --variant release",
"build:signed:aab": "tsx scripts/signed-build.ts",
"build:signed:apk": "tsx scripts/signed-build.ts --format apk",
"ios": "expo run:ios",
"web": "expo start --web",
"prebuild": "expo prebuild",
@@ -59,6 +61,7 @@
"devDependencies": {
"@epic-web/config": "^1.21.3",
"@types/react": "~19.1.12",
"cmd-ts": "^0.14.1",
"eslint": "^9.35.0",
"eslint-config-expo": "~10.0.0",
"jiti": "^2.5.1",

View File

@@ -0,0 +1,43 @@
import * as child from 'child_process';
import * as path from 'path';
export const cmd = (
command: string,
options: { relativeCwd?: string; stdio?: child.StdioOptions } = {},
) =>
new Promise<{ exitCode: number; stdout: string; stderr: string }>(
(resolve, reject) => {
console.log(`cmd: ${command}`);
const proc = child.spawn(command, {
shell: true,
stdio: options.stdio ?? 'inherit',
cwd: options.relativeCwd
? path.resolve(process.cwd(), options.relativeCwd)
: process.cwd(),
});
let stdout = '';
let stderr = '';
proc.stdout?.on('data', (data) => {
stdout += data;
});
proc.stderr?.on('data', (data) => {
stderr += data;
});
process.once('SIGTERM', () => {
proc.kill('SIGTERM');
});
process.once('SIGINT', () => {
proc.kill('SIGINT');
});
proc.on('close', (code) => {
console.log(`cmd: ${command} closed with code ${code}`);
resolve({ exitCode: code ?? 0, stdout, stderr });
});
proc.on('error', (error) => {
reject(error);
});
},
);

View File

@@ -0,0 +1,157 @@
/**
* https://docs.expo.dev/guides/local-app-production/
*/
import * as fsp from 'fs/promises';
import * as path from 'path';
import { command, run, option, oneOf } from 'cmd-ts';
import { z } from 'zod';
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 rawBwItem = JSON.parse(rawBwItemString);
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(rawBwItem, {
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',
}),
},
handler: async ({ format }) => {
{
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 (/release \{/.test(match)) {
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',
});
}
},
});
void run(signedBuildCommand, process.argv.slice(2));

20
pnpm-lock.yaml generated
View File

@@ -141,6 +141,9 @@ importers:
'@types/react':
specifier: ~19.1.12
version: 19.1.12
cmd-ts:
specifier: ^0.14.1
version: 0.14.1
eslint:
specifier: ^9.35.0
version: 9.35.0(jiti@2.5.1)
@@ -2271,6 +2274,9 @@ packages:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'}
cmd-ts@0.14.1:
resolution: {integrity: sha512-OXnLTdj0zKjOPAHLktkZUxyEquigm4o5LMufjFf+Eu3IJVxEZrG6oku69xAULaTS83p6bUb9iBP1gPXxomiCsg==}
color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
@@ -2474,6 +2480,9 @@ packages:
devalue@5.3.2:
resolution: {integrity: sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw==}
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
@@ -7920,6 +7929,15 @@ snapshots:
clone@1.0.4: {}
cmd-ts@0.14.1:
dependencies:
chalk: 5.6.2
debug: 4.4.1
didyoumean: 1.2.2
strip-ansi: 7.1.0
transitivePeerDependencies:
- supports-color
color-convert@1.9.3:
dependencies:
color-name: 1.1.3
@@ -8113,6 +8131,8 @@ snapshots:
devalue@5.3.2: {}
didyoumean@1.2.2: {}
doctrine@2.1.0:
dependencies:
esutils: 2.0.3