Builds Internos (Firebase)
Un solo comando compila la app localmente con el profile preview y la
sube a Firebase App Distribution.
Conectar los scripts
// package.json
{
"scripts": {
"release:preview:android": "node scripts/distribute-firebase.js android",
"release:preview:ios": "node scripts/distribute-firebase.js ios"
}
}
Qué hace el script
- Valida el
.env— falla rápido con un mensaje claro si falta una variableFIREBASE_*, antes de perder tiempo con un build. - Compila localmente con el profile
preview(eas build --local --non-interactive). - Verifica que el artefacto exista en
./build/. - Sube a Firebase con notas de release automáticas — la rama actual, el SHA corto del commit, y el asunto del último commit.
Acá está el scripts/distribute-firebase.js completo:
#!/usr/bin/env node
const { execSync } = require("node:child_process");
const fs = require("node:fs");
const path = require("node:path");
const platform = process.argv[2];
if (platform !== "android" && platform !== "ios") {
console.error("Usage: node scripts/distribute-firebase.js <android|ios>");
process.exit(1);
}
// Minimal .env loader (no dotenv dependency)
(function loadEnv() {
const envPath = path.resolve(process.cwd(), ".env");
if (!fs.existsSync(envPath)) return;
for (const line of fs.readFileSync(envPath, "utf8").split("\n")) {
const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
if (m && process.env[m[1]] === undefined) {
process.env[m[1]] = m[2].replace(/^["']|["']$/g, "");
}
}
})();
const appId =
platform === "android"
? process.env.FIREBASE_ANDROID_APP_ID
: process.env.FIREBASE_IOS_APP_ID;
const group = process.env.FIREBASE_TESTER_GROUP || "internal";
if (!appId) {
const varName =
platform === "android" ? "FIREBASE_ANDROID_APP_ID" : "FIREBASE_IOS_APP_ID";
console.error(`Missing ${varName} in .env. Copy scripts/.env.example to .env.`);
process.exit(1);
}
function git(cmd) {
return execSync(`git ${cmd}`, { encoding: "utf8" }).trim();
}
let releaseNotes = "manual build";
try {
releaseNotes = `${git("rev-parse --abbrev-ref HEAD")} ${git(
"rev-parse --short HEAD",
)} - ${git("log -1 --pretty=%s")}`;
} catch {}
function run(command) {
execSync(command, { stdio: "inherit" });
}
const ext = platform === "android" ? "apk" : "ipa";
const artifact = path.resolve("build", `app-preview.${ext}`);
fs.mkdirSync(path.dirname(artifact), { recursive: true });
console.log(`\n> Building ${platform} (preview, local)...`);
run(
`npx eas build -p ${platform} --profile preview --local --non-interactive ` +
`--output "${artifact}"`,
);
if (!fs.existsSync(artifact)) {
console.error(`Build finished but ${artifact} was not created. Aborting.`);
process.exit(1);
}
console.log(`\n> Uploading to Firebase App Distribution (group: ${group})...`);
run(
`npx firebase appdistribution:distribute "${artifact}" ` +
`--app "${appId}" --groups "${group}" ` +
`--release-notes "${releaseNotes.replace(/"/g, "'")}"`,
);
if (platform === "ios") {
console.log(
"\nNote: iOS is ad-hoc. A tester can only install if their device UDID is " +
"in the provisioning profile. New device? See 'Adding a New iOS Device'.",
);
}
console.log("\nDone.");
Android
Sin restricciones de dispositivo — un solo comando:
pnpm run release:preview:android
El release aparece en Firebase Console → App Distribution (app de Android), y los testers de tu grupo reciben un email con un link de instalación.
iOS
iOS usa distribución ad-hoc, que tiene reglas de dispositivo estrictas. Corré:
pnpm run release:preview:ios
…pero leé Agregar un Nuevo Dispositivo iOS antes de prometerle un build a un tester cuyo dispositivo todavía no registraste.
Instrucciones para el tester
- Android: abrir el email de Firebase → tocar el link → instalar el
.apk(permitir “instalar de fuentes desconocidas” si lo pide). - iOS: abrir el link del email → instalar la app Firebase App Tester (la ofrece ahí mismo) → instalar el build desde App Tester.
Advertencia
Android: un tester que tiene instalada la versión de tienda tiene que desinstalarla primero — el build interno está firmado de forma distinta, y Android no instala sobre una firma diferente.