liftbay 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/index.js +345 -89
- package/package.json +1 -1
- package/skills/liftbay/SKILL.md +43 -9
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ It's published at [liftbay/skills](https://github.com/liftbay/skills), and ships
|
|
|
31
31
|
| `builds list`, `builds view`, `builds logs`, `builds download`, `builds cancel` | Look at builds and their artifacts |
|
|
32
32
|
| `submit`, `submissions list`, `submissions view` | TestFlight and Google Play uploads |
|
|
33
33
|
| `device add`, `device list`, `device remove` | iOS test devices for ad hoc builds |
|
|
34
|
+
| `env list`, `env import`, `env set`, `env unset`, `env export` | Environment variables; `env import` reads `.env` files and `eas.json` |
|
|
34
35
|
| `credentials`, `credentials ios` | Show signing; set up iOS signing with your Apple ID or a `.p8` key |
|
|
35
36
|
| `update publish`, `update list`, `update rollout`, `update rollback` | Over-the-air updates |
|
|
36
37
|
| `channels` | List update channels |
|
package/dist/index.js
CHANGED
|
@@ -38,8 +38,8 @@ var require_picocolors = __commonJS({
|
|
|
38
38
|
"use strict";
|
|
39
39
|
var p = process || {};
|
|
40
40
|
var argv = p.argv || [];
|
|
41
|
-
var
|
|
42
|
-
var isColorSupported = !(!!
|
|
41
|
+
var env2 = p.env || {};
|
|
42
|
+
var isColorSupported = !(!!env2.NO_COLOR || argv.includes("--no-color")) && (!!env2.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env2.TERM !== "dumb" || !!env2.CI);
|
|
43
43
|
var formatter = (open, close, replace = open) => (input) => {
|
|
44
44
|
let string = "" + input, index = string.indexOf(close, open.length);
|
|
45
45
|
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
@@ -4575,10 +4575,10 @@ var require_browser = __commonJS({
|
|
|
4575
4575
|
text2 = canvas;
|
|
4576
4576
|
canvas = void 0;
|
|
4577
4577
|
}
|
|
4578
|
-
return new Promise(function(
|
|
4578
|
+
return new Promise(function(resolve4, reject) {
|
|
4579
4579
|
try {
|
|
4580
4580
|
const data = QRCode3.create(text2, opts);
|
|
4581
|
-
|
|
4581
|
+
resolve4(renderFunc(data, canvas, opts));
|
|
4582
4582
|
} catch (e) {
|
|
4583
4583
|
reject(e);
|
|
4584
4584
|
}
|
|
@@ -4660,11 +4660,11 @@ var require_server = __commonJS({
|
|
|
4660
4660
|
}
|
|
4661
4661
|
function render(renderFunc, text2, params) {
|
|
4662
4662
|
if (!params.cb) {
|
|
4663
|
-
return new Promise(function(
|
|
4663
|
+
return new Promise(function(resolve4, reject) {
|
|
4664
4664
|
try {
|
|
4665
4665
|
const data = QRCode3.create(text2, params.opts);
|
|
4666
4666
|
return renderFunc(data, params.opts, function(err, data2) {
|
|
4667
|
-
return err ? reject(err) :
|
|
4667
|
+
return err ? reject(err) : resolve4(data2);
|
|
4668
4668
|
});
|
|
4669
4669
|
} catch (e) {
|
|
4670
4670
|
reject(e);
|
|
@@ -28647,11 +28647,11 @@ async function detectApp(cwd = process.cwd()) {
|
|
|
28647
28647
|
appJson: existsSync4(join5(cwd, "app.json"))
|
|
28648
28648
|
};
|
|
28649
28649
|
}
|
|
28650
|
-
async function run(cmd, args, cwd = process.cwd(),
|
|
28651
|
-
const code = await new Promise((
|
|
28652
|
-
const child = spawn2(cmd, args, { cwd, stdio: "inherit", env: { ...process.env, ...
|
|
28650
|
+
async function run(cmd, args, cwd = process.cwd(), env2 = {}) {
|
|
28651
|
+
const code = await new Promise((resolve4, reject) => {
|
|
28652
|
+
const child = spawn2(cmd, args, { cwd, stdio: "inherit", env: { ...process.env, ...env2 } });
|
|
28653
28653
|
child.on("error", reject);
|
|
28654
|
-
child.on("close",
|
|
28654
|
+
child.on("close", resolve4);
|
|
28655
28655
|
});
|
|
28656
28656
|
if (code !== 0) fail(`\`${cmd} ${args.join(" ")}\` failed (exit ${code})`);
|
|
28657
28657
|
}
|
|
@@ -28672,6 +28672,22 @@ function cancelled(value) {
|
|
|
28672
28672
|
}
|
|
28673
28673
|
return value;
|
|
28674
28674
|
}
|
|
28675
|
+
var slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
28676
|
+
async function createFirstOrg(lb, given) {
|
|
28677
|
+
const me = await unwrap(lb.GET("/v1/auth/me"));
|
|
28678
|
+
const suggested = given ?? (me.user.name?.trim() || me.user.email.split("@")[0]);
|
|
28679
|
+
const name = isInteractive() && !given ? cancelled(await text({ message: "Name your organization", initialValue: suggested, validate: (v) => slugify(v ?? "").length >= 2 ? void 0 : "At least 2 letters or digits" })) : suggested;
|
|
28680
|
+
const base = slugify(name).padEnd(2, "0");
|
|
28681
|
+
for (const slug of [base, `${base.slice(0, 35)}-${Math.random().toString(36).slice(2, 6)}`]) {
|
|
28682
|
+
const res = await lb.POST("/v1/orgs", { body: { name: name.slice(0, 64), slug } });
|
|
28683
|
+
if (res.data) {
|
|
28684
|
+
log.success(`Created organization ${import_picocolors3.default.bold(res.data.name)} ${import_picocolors3.default.dim(`(liftbay.dev/${res.data.slug})`)}`);
|
|
28685
|
+
return res.data;
|
|
28686
|
+
}
|
|
28687
|
+
if (res.response.status !== 409) await unwrap(Promise.resolve(res));
|
|
28688
|
+
}
|
|
28689
|
+
return fail("Couldn't pick a free organization URL. Pass --org <slug>.");
|
|
28690
|
+
}
|
|
28675
28691
|
async function ensureLinked(app, opts) {
|
|
28676
28692
|
const existing = readConfig();
|
|
28677
28693
|
if (existing) {
|
|
@@ -28680,7 +28696,7 @@ async function ensureLinked(app, opts) {
|
|
|
28680
28696
|
}
|
|
28681
28697
|
const lb = client();
|
|
28682
28698
|
const orgs = await unwrap(lb.GET("/v1/orgs"));
|
|
28683
|
-
if (!orgs.length)
|
|
28699
|
+
if (!orgs.length) orgs.push(await createFirstOrg(lb, opts.org));
|
|
28684
28700
|
let org = opts.org ?? (orgs.length === 1 ? orgs[0].slug : void 0);
|
|
28685
28701
|
if (!org) {
|
|
28686
28702
|
if (!isInteractive()) fail(`Several organizations available; pass --org (${orgs.map((o) => o.slug).join(", ")})`);
|
|
@@ -29411,9 +29427,9 @@ async function deviceRemove(id) {
|
|
|
29411
29427
|
}
|
|
29412
29428
|
|
|
29413
29429
|
// src/commands/setup.ts
|
|
29414
|
-
import { existsSync as
|
|
29430
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
29415
29431
|
import { dirname as dirname3 } from "path";
|
|
29416
|
-
var
|
|
29432
|
+
var import_picocolors11 = __toESM(require_picocolors(), 1);
|
|
29417
29433
|
|
|
29418
29434
|
// src/generated/workflow.ts
|
|
29419
29435
|
var WORKFLOW_YAML = `# Liftbay builds on your own GitHub Actions minutes.
|
|
@@ -29567,6 +29583,219 @@ jobs:
|
|
|
29567
29583
|
run: '"$RUNNER_TEMP/liftbay-runner" run'
|
|
29568
29584
|
`;
|
|
29569
29585
|
|
|
29586
|
+
// src/commands/env.ts
|
|
29587
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
29588
|
+
import { basename as basename2, join as join8, resolve as resolve3 } from "path";
|
|
29589
|
+
var import_picocolors10 = __toESM(require_picocolors(), 1);
|
|
29590
|
+
var VISIBILITIES = ["plain", "sensitive", "secret"];
|
|
29591
|
+
var KEY = /^[A-Z_][A-Z0-9_]*$/;
|
|
29592
|
+
function parseEnv(content) {
|
|
29593
|
+
const vars = [];
|
|
29594
|
+
const invalid = [];
|
|
29595
|
+
content.split(/\r?\n/).forEach((raw, i2) => {
|
|
29596
|
+
const line = raw.trim();
|
|
29597
|
+
if (!line || line.startsWith("#")) return;
|
|
29598
|
+
const m2 = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
|
|
29599
|
+
if (!m2) return void invalid.push(i2 + 1);
|
|
29600
|
+
let value = m2[2] ?? "";
|
|
29601
|
+
const q = value[0];
|
|
29602
|
+
if ((q === '"' || q === "'") && value.length >= 2 && value.endsWith(q)) {
|
|
29603
|
+
value = value.slice(1, -1);
|
|
29604
|
+
if (q === '"') value = value.replace(/\\(n|"|\\)/g, (_, c2) => c2 === "n" ? "\n" : c2);
|
|
29605
|
+
} else {
|
|
29606
|
+
const hash = value.indexOf(" #");
|
|
29607
|
+
if (hash >= 0) value = value.slice(0, hash).trimEnd();
|
|
29608
|
+
}
|
|
29609
|
+
vars.push({ key: m2[1], value });
|
|
29610
|
+
});
|
|
29611
|
+
return { vars, invalid };
|
|
29612
|
+
}
|
|
29613
|
+
function serializeEnv(vars) {
|
|
29614
|
+
return vars.map(({ key, value }) => {
|
|
29615
|
+
const quote = /[\s#"'\\]/.test(value) || value === "";
|
|
29616
|
+
return `${key}=${quote ? `"${value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/"/g, '\\"')}"` : value}`;
|
|
29617
|
+
}).join("\n");
|
|
29618
|
+
}
|
|
29619
|
+
function visibilityFor(key, forced) {
|
|
29620
|
+
if (key.startsWith("EXPO_PUBLIC_")) return forced === "sensitive" ? "sensitive" : "plain";
|
|
29621
|
+
if (forced) return forced;
|
|
29622
|
+
return /(TOKEN|SECRET|PASSWORD|PASSWD|PRIVATE|CREDENTIAL|AUTH|API_KEY|ACCESS_KEY|_KEY$)/.test(key) ? "secret" : "sensitive";
|
|
29623
|
+
}
|
|
29624
|
+
function parseVisibility(v) {
|
|
29625
|
+
if (v && !VISIBILITIES.includes(v)) fail(`--visibility must be ${VISIBILITIES.join(", ")}`);
|
|
29626
|
+
return v;
|
|
29627
|
+
}
|
|
29628
|
+
var slugify2 = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "default";
|
|
29629
|
+
function readEasProfiles(cwd = process.cwd()) {
|
|
29630
|
+
const path2 = join8(cwd, "eas.json");
|
|
29631
|
+
if (!existsSync6(path2)) return null;
|
|
29632
|
+
let eas;
|
|
29633
|
+
try {
|
|
29634
|
+
eas = JSON.parse(readFileSync7(path2, "utf8"));
|
|
29635
|
+
} catch {
|
|
29636
|
+
fail("eas.json isn't valid JSON");
|
|
29637
|
+
}
|
|
29638
|
+
const build2 = eas.build ?? {};
|
|
29639
|
+
const resolveProfile = (name, seen = []) => {
|
|
29640
|
+
const own = build2[name] ?? fail(`eas.json: profile "${seen.at(-1)}" extends unknown profile "${name}"`);
|
|
29641
|
+
if (seen.includes(name)) fail(`eas.json: profiles extend each other in a loop (${[...seen, name].join(" \u2192 ")})`);
|
|
29642
|
+
const parent = own.extends ? resolveProfile(own.extends, [...seen, name]) : {};
|
|
29643
|
+
return { ...parent, ...own, env: { ...parent.env, ...own.env } };
|
|
29644
|
+
};
|
|
29645
|
+
const extended = new Set(Object.values(build2).map((b) => b.extends).filter(Boolean));
|
|
29646
|
+
return Object.keys(build2).filter((name) => !(extended.has(name) && !build2[name].channel && !build2[name].distribution && !build2[name].developmentClient)).map((name) => {
|
|
29647
|
+
const r2 = resolveProfile(name);
|
|
29648
|
+
const env2 = Object.fromEntries(Object.entries(r2.env ?? {}).filter(([k2, v]) => KEY.test(k2) && v !== void 0 && v !== null).map(([k2, v]) => [k2, String(v)]));
|
|
29649
|
+
return { name, slug: slugify2(name), distribution: r2.distribution === "internal" ? "internal" : "store", channel: slugify2(r2.channel ?? name), env: env2, developmentClient: !!r2.developmentClient };
|
|
29650
|
+
});
|
|
29651
|
+
}
|
|
29652
|
+
async function environments(lb, path2) {
|
|
29653
|
+
return unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/environments", { params: { path: path2 } }));
|
|
29654
|
+
}
|
|
29655
|
+
async function ensureEnvironment(lb, path2, slug, existing) {
|
|
29656
|
+
const envs = existing ?? await environments(lb, path2);
|
|
29657
|
+
if (envs.some((e) => e.slug === slug)) return false;
|
|
29658
|
+
await unwrap(lb.POST("/v1/orgs/{org}/projects/{project}/environments", { params: { path: path2 }, body: { name: slug, slug } }));
|
|
29659
|
+
return true;
|
|
29660
|
+
}
|
|
29661
|
+
async function importVariables(lb, path2, env2, vars, opts) {
|
|
29662
|
+
const result = { environment: env2, added: [], updated: [], skipped: [], visibility: {} };
|
|
29663
|
+
const bad = vars.filter((v) => !KEY.test(v.key));
|
|
29664
|
+
for (const v of bad) result.skipped.push({ key: v.key, reason: "names must be UPPER_SNAKE_CASE" });
|
|
29665
|
+
const byVisibility = /* @__PURE__ */ new Map();
|
|
29666
|
+
for (const v of vars.filter((v2) => KEY.test(v2.key))) {
|
|
29667
|
+
const vis = visibilityFor(v.key, opts.visibility);
|
|
29668
|
+
result.visibility[v.key] = vis;
|
|
29669
|
+
byVisibility.set(vis, [...(byVisibility.get(vis) ?? []).filter((x) => x.key !== v.key), v]);
|
|
29670
|
+
}
|
|
29671
|
+
for (const [visibility, group] of byVisibility) {
|
|
29672
|
+
const r2 = await unwrap(
|
|
29673
|
+
lb.POST("/v1/orgs/{org}/projects/{project}/environments/{env}/import", { params: { path: { ...path2, env: env2 } }, body: { content: serializeEnv(group), visibility, overwrite: opts.overwrite } })
|
|
29674
|
+
);
|
|
29675
|
+
result.added.push(...r2.added);
|
|
29676
|
+
result.updated.push(...r2.updated);
|
|
29677
|
+
result.skipped.push(...r2.skipped);
|
|
29678
|
+
}
|
|
29679
|
+
return result;
|
|
29680
|
+
}
|
|
29681
|
+
function printImport(r2) {
|
|
29682
|
+
const count = (n3, word) => `${n3} ${word}`;
|
|
29683
|
+
log.success(`${import_picocolors10.default.bold(r2.environment)}: ${count(r2.added.length, "added")}, ${count(r2.updated.length, "updated")}${r2.skipped.length ? `, ${r2.skipped.length} skipped` : ""}`);
|
|
29684
|
+
for (const key of [...r2.added, ...r2.updated]) log.message(` ${key.padEnd(36)} ${import_picocolors10.default.dim(r2.visibility[key] ?? "")}`);
|
|
29685
|
+
for (const s of r2.skipped) log.message(import_picocolors10.default.dim(` ${s.key.padEnd(36)} skipped: ${s.reason}`));
|
|
29686
|
+
}
|
|
29687
|
+
async function pickEnvironment(lb, path2, given, file2) {
|
|
29688
|
+
const envs = await environments(lb, path2);
|
|
29689
|
+
const fromName = file2 ? basename2(file2).match(/^\.env\.([a-z0-9-]+?)(?:\.local)?$/)?.[1] : void 0;
|
|
29690
|
+
const slug = given ?? (fromName && envs.some((e) => e.slug === fromName) ? fromName : void 0);
|
|
29691
|
+
if (slug) return { slug, envs };
|
|
29692
|
+
const names = envs.map((e) => e.slug).join(", ");
|
|
29693
|
+
if (!isInteractive()) fail(`Pass --environment (${names})${file2 ? ` for ${file2}` : ""}`);
|
|
29694
|
+
return { slug: cancelled(await select({ message: file2 ? `Environment for ${file2}` : "Environment", options: envs.map((e) => ({ value: e.slug, label: e.name, hint: `${e.variableCount} variables` })) })), envs };
|
|
29695
|
+
}
|
|
29696
|
+
var projectPath = () => {
|
|
29697
|
+
const config = requireConfig();
|
|
29698
|
+
return { org: config.org, project: config.project };
|
|
29699
|
+
};
|
|
29700
|
+
async function envList(opts) {
|
|
29701
|
+
const lb = client();
|
|
29702
|
+
const path2 = projectPath();
|
|
29703
|
+
if (!opts.environment) {
|
|
29704
|
+
const envs = await environments(lb, path2);
|
|
29705
|
+
for (const e of envs) console.log(`${import_picocolors10.default.bold(e.slug.padEnd(16))} ${String(e.variableCount).padStart(3)} variables ${import_picocolors10.default.dim(`revision ${e.revision}`)}`);
|
|
29706
|
+
return { environments: envs.map(({ slug, name, variableCount, revision, updatedAt }) => ({ slug, name, variableCount, revision, updatedAt })) };
|
|
29707
|
+
}
|
|
29708
|
+
const vars = await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/environments/{env}/variables", { params: { path: { ...path2, env: opts.environment } } }));
|
|
29709
|
+
if (!vars.length) console.log(`No variables in ${opts.environment}. Add some with ${import_picocolors10.default.cyan(`liftbay env import .env --environment ${opts.environment}`)}`);
|
|
29710
|
+
for (const v of vars) {
|
|
29711
|
+
const shown = v.visibility === "plain" ? v.value ?? "" : v.visibility === "secret" ? import_picocolors10.default.dim("(secret)") : import_picocolors10.default.dim("\u2022\u2022\u2022\u2022\u2022\u2022 (sensitive)");
|
|
29712
|
+
console.log(`${v.key.padEnd(36)} ${shown}`);
|
|
29713
|
+
}
|
|
29714
|
+
return { environment: opts.environment, variables: vars.map((v) => ({ key: v.key, visibility: v.visibility, value: v.visibility === "plain" ? v.value : null, updatedAt: v.updatedAt })) };
|
|
29715
|
+
}
|
|
29716
|
+
async function envSet(pairs, opts) {
|
|
29717
|
+
const lb = client();
|
|
29718
|
+
const path2 = projectPath();
|
|
29719
|
+
const vars = pairs.map((pair) => {
|
|
29720
|
+
const at = pair.indexOf("=");
|
|
29721
|
+
if (at < 1) fail(`"${pair}" isn't KEY=value`);
|
|
29722
|
+
return { key: pair.slice(0, at), value: pair.slice(at + 1) };
|
|
29723
|
+
});
|
|
29724
|
+
const { slug } = await pickEnvironment(lb, path2, opts.environment);
|
|
29725
|
+
const r2 = await importVariables(lb, path2, slug, vars, { visibility: parseVisibility(opts.visibility), overwrite: true });
|
|
29726
|
+
printImport(r2);
|
|
29727
|
+
return r2;
|
|
29728
|
+
}
|
|
29729
|
+
async function envUnset(keys, opts) {
|
|
29730
|
+
const lb = client();
|
|
29731
|
+
const path2 = projectPath();
|
|
29732
|
+
const { slug } = await pickEnvironment(lb, path2, opts.environment);
|
|
29733
|
+
const vars = await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/environments/{env}/variables", { params: { path: { ...path2, env: slug } } }));
|
|
29734
|
+
const removed = [];
|
|
29735
|
+
for (const key of keys) {
|
|
29736
|
+
const v = vars.find((x) => x.key === key);
|
|
29737
|
+
if (!v) {
|
|
29738
|
+
log.warn(`${key} isn't set in ${slug}`);
|
|
29739
|
+
continue;
|
|
29740
|
+
}
|
|
29741
|
+
await unwrap(lb.DELETE("/v1/orgs/{org}/projects/{project}/environments/{env}/variables/{id}", { params: { path: { ...path2, env: slug, id: v.id } } }));
|
|
29742
|
+
log.success(`Removed ${key} from ${slug}`);
|
|
29743
|
+
removed.push(key);
|
|
29744
|
+
}
|
|
29745
|
+
return { environment: slug, removed };
|
|
29746
|
+
}
|
|
29747
|
+
async function envImport(files, opts) {
|
|
29748
|
+
const lb = client();
|
|
29749
|
+
const path2 = projectPath();
|
|
29750
|
+
const visibility = parseVisibility(opts.visibility);
|
|
29751
|
+
const overwrite = opts.overwrite !== false;
|
|
29752
|
+
const results = [];
|
|
29753
|
+
for (const file2 of files) {
|
|
29754
|
+
const abs = resolve3(file2.replace(/^~/, process.env.HOME ?? "~"));
|
|
29755
|
+
if (!existsSync6(abs)) fail(`No file at ${file2}`);
|
|
29756
|
+
if (basename2(abs) === "eas.json") {
|
|
29757
|
+
const profiles = readEasProfiles(resolve3(abs, ".."));
|
|
29758
|
+
if (!profiles.length) fail("eas.json has no build profiles");
|
|
29759
|
+
const envs = await environments(lb, path2);
|
|
29760
|
+
for (const prof of opts.environment ? profiles.filter((x) => x.slug === opts.environment) : profiles) {
|
|
29761
|
+
if (await ensureEnvironment(lb, path2, prof.slug, envs)) log.success(`Created environment ${import_picocolors10.default.bold(prof.slug)}`);
|
|
29762
|
+
const vars2 = Object.entries(prof.env).map(([key, value]) => ({ key, value }));
|
|
29763
|
+
if (!vars2.length) {
|
|
29764
|
+
log.info(`${import_picocolors10.default.bold(prof.slug)}: no env in eas.json`);
|
|
29765
|
+
continue;
|
|
29766
|
+
}
|
|
29767
|
+
const r3 = await importVariables(lb, path2, prof.slug, vars2, { visibility, overwrite });
|
|
29768
|
+
printImport(r3);
|
|
29769
|
+
results.push(r3);
|
|
29770
|
+
}
|
|
29771
|
+
continue;
|
|
29772
|
+
}
|
|
29773
|
+
const { vars, invalid } = parseEnv(readFileSync7(abs, "utf8"));
|
|
29774
|
+
if (invalid.length) log.warn(`${file2}: skipped unreadable line${invalid.length > 1 ? "s" : ""} ${invalid.join(", ")}`);
|
|
29775
|
+
const { slug } = await pickEnvironment(lb, path2, opts.environment, file2);
|
|
29776
|
+
const r2 = await importVariables(lb, path2, slug, vars, { visibility, overwrite });
|
|
29777
|
+
printImport(r2);
|
|
29778
|
+
results.push(r2);
|
|
29779
|
+
}
|
|
29780
|
+
return { imported: results };
|
|
29781
|
+
}
|
|
29782
|
+
async function envExport(opts) {
|
|
29783
|
+
const lb = client();
|
|
29784
|
+
const path2 = projectPath();
|
|
29785
|
+
const { slug } = await pickEnvironment(lb, path2, opts.environment);
|
|
29786
|
+
const content = String(await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/environments/{env}/export", { params: { path: { ...path2, env: slug } }, parseAs: "text" })));
|
|
29787
|
+
if (!opts.out) {
|
|
29788
|
+
process.stdout.write(`${content.trimEnd()}
|
|
29789
|
+
`);
|
|
29790
|
+
return { environment: slug, content };
|
|
29791
|
+
}
|
|
29792
|
+
if (existsSync6(opts.out) && !opts.force) fail(`${opts.out} exists. Pass --force to overwrite it.`);
|
|
29793
|
+
writeFileSync6(opts.out, `${content.trimEnd()}
|
|
29794
|
+
`, { mode: 384 });
|
|
29795
|
+
log.success(`Wrote ${opts.out} ${import_picocolors10.default.dim("(secret values are not exported)")}`);
|
|
29796
|
+
return { environment: slug, file: resolve3(opts.out) };
|
|
29797
|
+
}
|
|
29798
|
+
|
|
29570
29799
|
// src/commands/setup.ts
|
|
29571
29800
|
var DEFAULT_PROFILES = {
|
|
29572
29801
|
development: { environment: "development", channel: "development", distribution: "internal" },
|
|
@@ -29595,11 +29824,11 @@ async function ciInit(app, config, opts) {
|
|
|
29595
29824
|
})
|
|
29596
29825
|
);
|
|
29597
29826
|
}
|
|
29598
|
-
if (platforms.includes("ios") && !app.bundleId) log.warn(`No iOS bundle identifier found. Set ${
|
|
29599
|
-
if (platforms.includes("android") && !app.androidPackage) log.warn(`No Android package found. Set ${
|
|
29827
|
+
if (platforms.includes("ios") && !app.bundleId) log.warn(`No iOS bundle identifier found. Set ${import_picocolors11.default.cyan("expo.ios.bundleIdentifier")} in app.json before the first iOS build.`);
|
|
29828
|
+
if (platforms.includes("android") && !app.androidPackage) log.warn(`No Android package found. Set ${import_picocolors11.default.cyan("expo.android.package")} in app.json before the first Android build.`);
|
|
29600
29829
|
const path2 = { org: config.org, project: config.project };
|
|
29601
29830
|
const repo = await githubRepo();
|
|
29602
|
-
if (!repo) log.warn(`No GitHub remote found. Builds run on GitHub Actions: push this app to GitHub, then run ${
|
|
29831
|
+
if (!repo) log.warn(`No GitHub remote found. Builds run on GitHub Actions: push this app to GitHub, then run ${import_picocolors11.default.cyan("liftbay ci init")} again.`);
|
|
29603
29832
|
await unwrap(
|
|
29604
29833
|
lb.PATCH("/v1/orgs/{org}/projects/{project}", {
|
|
29605
29834
|
params: { path: path2 },
|
|
@@ -29607,21 +29836,40 @@ async function ciInit(app, config, opts) {
|
|
|
29607
29836
|
})
|
|
29608
29837
|
);
|
|
29609
29838
|
await writeWorkflow(opts);
|
|
29610
|
-
const
|
|
29611
|
-
|
|
29612
|
-
|
|
29613
|
-
|
|
29614
|
-
}
|
|
29839
|
+
const eas = opts.eas === false ? null : readEasProfiles();
|
|
29840
|
+
let fromEas = false;
|
|
29841
|
+
if (eas?.length) {
|
|
29842
|
+
fromEas = opts.yes || !isInteractive() || cancelled(await confirm({ message: `Found eas.json (profiles ${eas.map((e) => e.name).join(", ")}). Use its build profiles, channels and env variables?`, initialValue: true }));
|
|
29615
29843
|
}
|
|
29616
|
-
const
|
|
29844
|
+
const base = fromEas ? Object.fromEntries(eas.map((e) => [e.name, { environment: e.slug, channel: e.channel, distribution: e.distribution }])) : DEFAULT_PROFILES;
|
|
29845
|
+
const envs = await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/environments", { params: { path: path2 } }));
|
|
29846
|
+
for (const profile of Object.values(base)) {
|
|
29847
|
+
if (await ensureEnvironment(lb, path2, profile.environment, envs)) envs.push({ slug: profile.environment });
|
|
29848
|
+
}
|
|
29849
|
+
const imported = [];
|
|
29850
|
+
if (fromEas) {
|
|
29851
|
+
log.step("Importing env variables from eas.json");
|
|
29852
|
+
for (const e of eas) {
|
|
29853
|
+
const vars = Object.entries(e.env).map(([key, value]) => ({ key, value }));
|
|
29854
|
+
if (!vars.length) continue;
|
|
29855
|
+
const r2 = await importVariables(lb, path2, e.slug, vars, { overwrite: false });
|
|
29856
|
+
printImport(r2);
|
|
29857
|
+
imported.push(r2);
|
|
29858
|
+
}
|
|
29859
|
+
log.info(import_picocolors11.default.dim("EXPO_PUBLIC_* values are plain (they're in the app bundle); names like *_TOKEN or *_SECRET are secret; the rest sensitive."));
|
|
29860
|
+
log.info(import_picocolors11.default.dim(`Variables kept on expo.dev (EAS environment variables) aren't in eas.json: ${import_picocolors11.default.cyan("npx eas env:pull --environment production")}, then ${import_picocolors11.default.cyan("liftbay env import .env.local --environment production")}.`));
|
|
29861
|
+
}
|
|
29862
|
+
const profiles = Object.fromEntries(Object.entries(base).map(([name, prof]) => [name, { ...prof, platforms }]));
|
|
29617
29863
|
const next = { ...config, build: { profiles, triggers: config.build?.triggers ?? [] } };
|
|
29618
29864
|
writeConfig(next);
|
|
29619
29865
|
note(
|
|
29620
|
-
Object.entries(profiles).map(([name, prof]) => `${
|
|
29866
|
+
Object.entries(profiles).map(([name, prof]) => `${import_picocolors11.default.bold(name.padEnd(12))} env ${prof.environment.padEnd(12)} ${prof.distribution === "store" ? "store" : "internal"} \xB7 ${prof.platforms.join(", ")}`).join("\n"),
|
|
29621
29867
|
"Build profiles"
|
|
29622
29868
|
);
|
|
29869
|
+
lastImport = imported;
|
|
29623
29870
|
return next;
|
|
29624
29871
|
}
|
|
29872
|
+
var lastImport = [];
|
|
29625
29873
|
var WORKFLOW_PATH = ".github/workflows/liftbay.yml";
|
|
29626
29874
|
async function githubRepo() {
|
|
29627
29875
|
try {
|
|
@@ -29632,16 +29880,16 @@ async function githubRepo() {
|
|
|
29632
29880
|
}
|
|
29633
29881
|
}
|
|
29634
29882
|
async function writeWorkflow(opts) {
|
|
29635
|
-
const current =
|
|
29883
|
+
const current = existsSync7(WORKFLOW_PATH) ? readFileSync8(WORKFLOW_PATH, "utf8") : null;
|
|
29636
29884
|
if (current === WORKFLOW_YAML) return;
|
|
29637
29885
|
if (current !== null) {
|
|
29638
29886
|
const replace = opts.yes || isInteractive() && cancelled(await confirm({ message: `${WORKFLOW_PATH} differs from the current Liftbay workflow. Replace it?` }));
|
|
29639
29887
|
if (!replace) return log.info(`Kept your ${WORKFLOW_PATH}.`);
|
|
29640
29888
|
}
|
|
29641
29889
|
mkdirSync5(dirname3(WORKFLOW_PATH), { recursive: true });
|
|
29642
|
-
|
|
29643
|
-
log.success(`Wrote ${
|
|
29644
|
-
log.info(
|
|
29890
|
+
writeFileSync7(WORKFLOW_PATH, WORKFLOW_YAML);
|
|
29891
|
+
log.success(`Wrote ${import_picocolors11.default.cyan(WORKFLOW_PATH)}. Commit and push it: Liftbay starts it for every build, on your GitHub Actions minutes.`);
|
|
29892
|
+
log.info(import_picocolors11.default.dim("GitHub counts macOS minutes about 10\xD7 faster than Linux; on a private repo with GitHub Free that is roughly 10 iOS builds a month."));
|
|
29645
29893
|
}
|
|
29646
29894
|
var pluginSpec = () => process.env.LIFTBAY_EXPO_PLUGIN_SPEC ?? "@liftbay/expo-plugin";
|
|
29647
29895
|
async function addDependency(app, spec) {
|
|
@@ -29662,7 +29910,8 @@ async function otaInit(app, config, opts) {
|
|
|
29662
29910
|
}
|
|
29663
29911
|
log.step("Installing the Liftbay config plugin");
|
|
29664
29912
|
await addDependency(app, pluginSpec());
|
|
29665
|
-
|
|
29913
|
+
const existingPolicy = readAppJson()?.expo?.runtimeVersion?.policy;
|
|
29914
|
+
let policy = opts.runtimePolicy ?? (existingPolicy === "appVersion" || existingPolicy === "fingerprint" ? existingPolicy : void 0);
|
|
29666
29915
|
if (!policy) {
|
|
29667
29916
|
policy = opts.yes || !isInteractive() ? "appVersion" : cancelled(
|
|
29668
29917
|
await select({
|
|
@@ -29679,16 +29928,16 @@ async function otaInit(app, config, opts) {
|
|
|
29679
29928
|
let channels2 = await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/channels", { params: { path: path2 } }));
|
|
29680
29929
|
for (const prof of Object.values(profiles)) {
|
|
29681
29930
|
const existing = channels2.find((c2) => c2.name === prof.channel);
|
|
29682
|
-
const
|
|
29683
|
-
if (!
|
|
29931
|
+
const env2 = envs.find((e) => e.slug === prof.environment);
|
|
29932
|
+
if (!env2) fail(`Environment ${prof.environment} not found; run \`liftbay ci init\` first`);
|
|
29684
29933
|
if (!existing) {
|
|
29685
|
-
await unwrap(lb.POST("/v1/orgs/{org}/projects/{project}/channels", { params: { path: path2 }, body: { name: prof.channel, environmentId:
|
|
29686
|
-
log.success(`Created channel ${
|
|
29934
|
+
await unwrap(lb.POST("/v1/orgs/{org}/projects/{project}/channels", { params: { path: path2 }, body: { name: prof.channel, environmentId: env2.id, protocols: ["expo"] } }));
|
|
29935
|
+
log.success(`Created channel ${import_picocolors11.default.bold(prof.channel)} \u2192 ${prof.environment}`);
|
|
29687
29936
|
} else if (!existing.protocols.includes("expo")) {
|
|
29688
29937
|
await unwrap(
|
|
29689
29938
|
lb.PATCH("/v1/orgs/{org}/projects/{project}/channels/{channel}", { params: { path: { ...path2, channel: prof.channel } }, body: { protocols: [...existing.protocols, "expo"] } })
|
|
29690
29939
|
);
|
|
29691
|
-
log.success(`Enabled expo-updates on channel ${
|
|
29940
|
+
log.success(`Enabled expo-updates on channel ${import_picocolors11.default.bold(prof.channel)}`);
|
|
29692
29941
|
}
|
|
29693
29942
|
}
|
|
29694
29943
|
channels2 = await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/channels", { params: { path: path2 } }));
|
|
@@ -29708,23 +29957,23 @@ runtimeVersion: { "policy": "${policy}" }`, "Add to your app config");
|
|
|
29708
29957
|
else plugins.push(entry);
|
|
29709
29958
|
expo.runtimeVersion = { policy };
|
|
29710
29959
|
writeAppJson(appJson);
|
|
29711
|
-
log.success(`Updated ${
|
|
29960
|
+
log.success(`Updated ${import_picocolors11.default.cyan("app.json")}: Liftbay plugin, runtimeVersion policy "${policy}"`);
|
|
29712
29961
|
}
|
|
29713
29962
|
const next = { ...config, updates: { client: "expo-updates", runtimeVersion: { policy } } };
|
|
29714
29963
|
writeConfig(next);
|
|
29715
29964
|
return next;
|
|
29716
29965
|
}
|
|
29717
29966
|
async function ciInitCommand(opts) {
|
|
29718
|
-
intro(
|
|
29967
|
+
intro(import_picocolors11.default.bgCyan(import_picocolors11.default.black(" liftbay ci init ")));
|
|
29719
29968
|
await ensureSignedIn(opts);
|
|
29720
29969
|
const app = await detectApp();
|
|
29721
29970
|
const config = await ensureLinked(app, opts);
|
|
29722
29971
|
const next = await ciInit(app, config, opts);
|
|
29723
|
-
outro(`Builds are configured. Start one with ${
|
|
29724
|
-
return { config: next };
|
|
29972
|
+
outro(`Builds are configured. Start one with ${import_picocolors11.default.cyan("liftbay build --profile preview")}`);
|
|
29973
|
+
return { config: next, imported: lastImport };
|
|
29725
29974
|
}
|
|
29726
29975
|
async function otaInitCommand(opts) {
|
|
29727
|
-
intro(
|
|
29976
|
+
intro(import_picocolors11.default.bgCyan(import_picocolors11.default.black(" liftbay ota init ")));
|
|
29728
29977
|
await ensureSignedIn(opts);
|
|
29729
29978
|
const app = await detectApp();
|
|
29730
29979
|
const config = await ensureLinked(app, opts);
|
|
@@ -29735,29 +29984,30 @@ async function otaInitCommand(opts) {
|
|
|
29735
29984
|
var nextStepsOta = () => `OTA updates are configured.
|
|
29736
29985
|
|
|
29737
29986
|
1. Build a new binary once (the update URL and channel are baked in)
|
|
29738
|
-
2. Ship JS changes with ${
|
|
29987
|
+
2. Ship JS changes with ${import_picocolors11.default.cyan("liftbay update publish --channel production")}`;
|
|
29739
29988
|
async function initCommand(opts) {
|
|
29740
|
-
intro(
|
|
29989
|
+
intro(import_picocolors11.default.bgCyan(import_picocolors11.default.black(" liftbay init ")));
|
|
29741
29990
|
await ensureSignedIn(opts);
|
|
29742
29991
|
const app = await detectApp();
|
|
29743
29992
|
log.info(
|
|
29744
|
-
`${
|
|
29993
|
+
`${import_picocolors11.default.bold(app.name)}: ${app.framework === "expo" ? "Expo" : "React Native"}${app.version ? ` ${app.version}` : ""}${app.bundleId ? ` \xB7 ${app.bundleId}` : ""}${app.androidPackage && app.androidPackage !== app.bundleId ? ` \xB7 ${app.androidPackage}` : ""}`
|
|
29745
29994
|
);
|
|
29746
29995
|
let config = await ensureLinked(app, opts);
|
|
29747
|
-
log.step(
|
|
29996
|
+
log.step(import_picocolors11.default.bold("1/2 Builds"));
|
|
29748
29997
|
config = await ciInit(app, config, opts);
|
|
29749
|
-
log.step(
|
|
29998
|
+
log.step(import_picocolors11.default.bold("2/2 OTA updates"));
|
|
29750
29999
|
const wantOta = opts.ota ?? (opts.yes || !isInteractive() ? true : cancelled(await confirm({ message: "Set up over-the-air updates with expo-updates?", initialValue: true })));
|
|
29751
30000
|
if (wantOta) config = await otaInit(await detectApp(), config, opts);
|
|
29752
|
-
else log.info(`Skipped. Run ${
|
|
30001
|
+
else log.info(`Skipped. Run ${import_picocolors11.default.cyan("liftbay ota init")} any time.`);
|
|
29753
30002
|
outro(
|
|
29754
|
-
`Done. ${
|
|
30003
|
+
`Done. ${import_picocolors11.default.dim(`${dashboardUrl()}/${config.org}/${config.project}`)}
|
|
29755
30004
|
|
|
29756
|
-
Build: ${
|
|
29757
|
-
Update: ${
|
|
30005
|
+
Build: ${import_picocolors11.default.cyan("liftbay build --profile preview")}${wantOta ? `
|
|
30006
|
+
Update: ${import_picocolors11.default.cyan("liftbay update publish --channel production")}` : ""}`
|
|
29758
30007
|
);
|
|
29759
30008
|
return {
|
|
29760
30009
|
config,
|
|
30010
|
+
imported: lastImport,
|
|
29761
30011
|
url: `${dashboardUrl()}/${config.org}/${config.project}`,
|
|
29762
30012
|
next: ["Commit liftbay.json, .github/workflows/liftbay.yml and app.json, and push", "liftbay build --profile preview", ...wantOta ? ["liftbay update publish --channel production"] : []]
|
|
29763
30013
|
};
|
|
@@ -29766,17 +30016,17 @@ async function initCommand(opts) {
|
|
|
29766
30016
|
// src/commands/update.ts
|
|
29767
30017
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
29768
30018
|
import { createHash as createHash4 } from "crypto";
|
|
29769
|
-
import { mkdtempSync, readFileSync as
|
|
30019
|
+
import { mkdtempSync, readFileSync as readFileSync10, rmSync as rmSync4 } from "fs";
|
|
29770
30020
|
import { tmpdir } from "os";
|
|
29771
|
-
import { join as
|
|
29772
|
-
var
|
|
30021
|
+
import { join as join9 } from "path";
|
|
30022
|
+
var import_picocolors13 = __toESM(require_picocolors(), 1);
|
|
29773
30023
|
|
|
29774
30024
|
// src/commands/signing.ts
|
|
29775
30025
|
import { createPublicKey, createSign, randomBytes as randomBytes2 } from "crypto";
|
|
29776
|
-
import { chmodSync as chmodSync4, existsSync as
|
|
30026
|
+
import { chmodSync as chmodSync4, existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
|
|
29777
30027
|
import { dirname as dirname4, relative } from "path";
|
|
29778
30028
|
var import_node_forge = __toESM(require_lib2(), 1);
|
|
29779
|
-
var
|
|
30029
|
+
var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
29780
30030
|
var DEFAULT_PRIVATE_KEY = "keys/liftbay-private-key.pem";
|
|
29781
30031
|
var DEFAULT_PUBLIC_KEY = "certs/liftbay-public-key.pem";
|
|
29782
30032
|
var DEFAULT_CERTIFICATE = "certs/liftbay-certificate.pem";
|
|
@@ -29806,18 +30056,18 @@ function generateSigningCertificate(commonName, years = 10) {
|
|
|
29806
30056
|
var signRsaSha256 = (privateKeyPem, data) => createSign("RSA-SHA256").update(data).sign(privateKeyPem, "base64");
|
|
29807
30057
|
function loadPrivateKey(path2) {
|
|
29808
30058
|
const read = (file2, label) => {
|
|
29809
|
-
if (!
|
|
29810
|
-
return { pem:
|
|
30059
|
+
if (!existsSync8(file2)) fail(`No private key at ${file2} (${label})`);
|
|
30060
|
+
return { pem: readFileSync9(file2, "utf8"), source: file2 };
|
|
29811
30061
|
};
|
|
29812
30062
|
if (path2) return read(path2, "--private-key");
|
|
29813
|
-
const
|
|
29814
|
-
if (
|
|
29815
|
-
return
|
|
30063
|
+
const env2 = process.env.LIFTBAY_UPDATE_PRIVATE_KEY;
|
|
30064
|
+
if (env2) return env2.includes("PRIVATE KEY") ? { pem: env2.replace(/\\n/g, "\n"), source: "LIFTBAY_UPDATE_PRIVATE_KEY" } : read(env2, "LIFTBAY_UPDATE_PRIVATE_KEY");
|
|
30065
|
+
return existsSync8(DEFAULT_PRIVATE_KEY) ? { pem: readFileSync9(DEFAULT_PRIVATE_KEY, "utf8"), source: DEFAULT_PRIVATE_KEY } : null;
|
|
29816
30066
|
}
|
|
29817
30067
|
function ensureIgnored(entry) {
|
|
29818
|
-
const current =
|
|
30068
|
+
const current = existsSync8(".gitignore") ? readFileSync9(".gitignore", "utf8") : "";
|
|
29819
30069
|
if (current.split("\n").some((l2) => l2.trim() === entry || l2.trim() === entry.replace(/\/$/, ""))) return false;
|
|
29820
|
-
|
|
30070
|
+
writeFileSync8(".gitignore", `${current}${current && !current.endsWith("\n") ? "\n" : ""}
|
|
29821
30071
|
# Liftbay update signing key: never commit it
|
|
29822
30072
|
${entry}
|
|
29823
30073
|
`);
|
|
@@ -29839,7 +30089,7 @@ async function signingInit(opts) {
|
|
|
29839
30089
|
const config = requireConfig();
|
|
29840
30090
|
const lb = client();
|
|
29841
30091
|
const path2 = { org: config.org, project: config.project };
|
|
29842
|
-
intro(
|
|
30092
|
+
intro(import_picocolors12.default.bgCyan(import_picocolors12.default.black(" liftbay update signing init ")));
|
|
29843
30093
|
const { signing: signing2 } = await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/updates/signing", { params: { path: path2 } }));
|
|
29844
30094
|
if (signing2 && !opts.force) {
|
|
29845
30095
|
fail(
|
|
@@ -29848,16 +30098,16 @@ async function signingInit(opts) {
|
|
|
29848
30098
|
}
|
|
29849
30099
|
const privateKeyPath = opts.privateKey ?? DEFAULT_PRIVATE_KEY;
|
|
29850
30100
|
const certificatePath = opts.certificate ?? DEFAULT_CERTIFICATE;
|
|
29851
|
-
if (
|
|
30101
|
+
if (existsSync8(privateKeyPath) && !opts.force) fail(`${privateKeyPath} already exists. Pass --force to overwrite it, or --private-key for another path.`);
|
|
29852
30102
|
const generated = generateSigningCertificate(opts.commonName ?? config.project);
|
|
29853
30103
|
for (const dir of /* @__PURE__ */ new Set([dirname4(privateKeyPath), dirname4(certificatePath), dirname4(DEFAULT_PUBLIC_KEY)])) mkdirSync6(dir, { recursive: true });
|
|
29854
|
-
|
|
30104
|
+
writeFileSync8(privateKeyPath, generated.privateKeyPem, { mode: 384 });
|
|
29855
30105
|
chmodSync4(privateKeyPath, 384);
|
|
29856
|
-
|
|
29857
|
-
|
|
29858
|
-
if (ensureIgnored(`${dirname4(relative(".", privateKeyPath))}/`)) log.info(`Added ${
|
|
29859
|
-
log.success(`Private key: ${
|
|
29860
|
-
Certificate: ${
|
|
30106
|
+
writeFileSync8(certificatePath, generated.certificatePem);
|
|
30107
|
+
writeFileSync8(DEFAULT_PUBLIC_KEY, generated.publicKeyPem);
|
|
30108
|
+
if (ensureIgnored(`${dirname4(relative(".", privateKeyPath))}/`)) log.info(`Added ${import_picocolors12.default.cyan(`${dirname4(privateKeyPath)}/`)} to .gitignore`);
|
|
30109
|
+
log.success(`Private key: ${import_picocolors12.default.cyan(privateKeyPath)} (stays on this machine)
|
|
30110
|
+
Certificate: ${import_picocolors12.default.cyan(certificatePath)} (commit it; it's embedded in your app)`);
|
|
29861
30111
|
await unwrap(
|
|
29862
30112
|
lb.PUT("/v1/orgs/{org}/projects/{project}/updates/signing", {
|
|
29863
30113
|
params: { path: path2 },
|
|
@@ -29865,7 +30115,7 @@ async function signingInit(opts) {
|
|
|
29865
30115
|
})
|
|
29866
30116
|
);
|
|
29867
30117
|
log.success("Certificate saved to Liftbay");
|
|
29868
|
-
if (configureAppJson(certificatePath)) log.success(`Updated ${
|
|
30118
|
+
if (configureAppJson(certificatePath)) log.success(`Updated ${import_picocolors12.default.cyan("app.json")}: the Liftbay plugin embeds the certificate in your app`);
|
|
29869
30119
|
else note(`plugins: [["@liftbay/expo-plugin", { "codeSigningCertificate": "./${certificatePath}" }]]`, "Add to your app config");
|
|
29870
30120
|
const channels2 = await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/channels", { params: { path: path2 } }));
|
|
29871
30121
|
const signedOnly = !!channels2.length && (opts.yes || !isInteractive() || cancelled(await confirm({ message: "Require signed updates on every channel? Unsigned publishes will be refused.", initialValue: true })));
|
|
@@ -29878,12 +30128,12 @@ async function signingInit(opts) {
|
|
|
29878
30128
|
note(
|
|
29879
30129
|
[
|
|
29880
30130
|
`1. Rebuild your apps once: the certificate is part of the binary.`,
|
|
29881
|
-
`2. ${
|
|
29882
|
-
`3. In CI, store the private key as a secret named ${
|
|
30131
|
+
`2. ${import_picocolors12.default.cyan("liftbay update publish")} signs automatically with ${privateKeyPath}.`,
|
|
30132
|
+
`3. In CI, store the private key as a secret named ${import_picocolors12.default.cyan("LIFTBAY_UPDATE_PRIVATE_KEY")}.`,
|
|
29883
30133
|
`4. Back the key up somewhere safe. If it's lost, apps need a rebuild with a new certificate.`,
|
|
29884
30134
|
``,
|
|
29885
|
-
`hot-updater: set ${
|
|
29886
|
-
`CodePush: set ${
|
|
30135
|
+
`hot-updater: set ${import_picocolors12.default.cyan("HOT_UPDATER_PUBLIC_KEY")} (iOS Info.plist) / ${import_picocolors12.default.cyan("com.hotupdater.PUBLIC_KEY")} (Android) to ${DEFAULT_PUBLIC_KEY}.`,
|
|
30136
|
+
`CodePush: set ${import_picocolors12.default.cyan("CodePushPublicKey")} (iOS) / ${import_picocolors12.default.cyan("CodePushPublicKey")} string resource (Android) to ${DEFAULT_PUBLIC_KEY}.`
|
|
29887
30137
|
].join("\n"),
|
|
29888
30138
|
"Next"
|
|
29889
30139
|
);
|
|
@@ -29914,13 +30164,13 @@ var contentTypes = {
|
|
|
29914
30164
|
mp4: "video/mp4"
|
|
29915
30165
|
};
|
|
29916
30166
|
function file(dir, rel, virtualPath, contentType) {
|
|
29917
|
-
const data =
|
|
29918
|
-
return { path: virtualPath, abs:
|
|
30167
|
+
const data = readFileSync10(join9(dir, rel));
|
|
30168
|
+
return { path: virtualPath, abs: join9(dir, rel), sha256: createHash4("sha256").update(data).digest("hex"), sizeBytes: data.length, contentType };
|
|
29919
30169
|
}
|
|
29920
30170
|
async function exportBundles(platforms) {
|
|
29921
|
-
const out = mkdtempSync(
|
|
30171
|
+
const out = mkdtempSync(join9(tmpdir(), "liftbay-export-"));
|
|
29922
30172
|
await run("npx", ["expo", "export", "--output-dir", out, "--dump-assetmap", ...platforms.flatMap((pl) => ["--platform", pl])]);
|
|
29923
|
-
const meta = JSON.parse(
|
|
30173
|
+
const meta = JSON.parse(readFileSync10(join9(out, "metadata.json"), "utf8"));
|
|
29924
30174
|
const bundles = {};
|
|
29925
30175
|
for (const pl of platforms) {
|
|
29926
30176
|
const m2 = meta.fileMetadata[pl];
|
|
@@ -29959,7 +30209,7 @@ async function publish(opts) {
|
|
|
29959
30209
|
return null;
|
|
29960
30210
|
}
|
|
29961
30211
|
})();
|
|
29962
|
-
intro(
|
|
30212
|
+
intro(import_picocolors13.default.bgCyan(import_picocolors13.default.black(" liftbay update publish ")));
|
|
29963
30213
|
if (!platforms.every((x) => x === "ios" || x === "android")) fail("--platform must be ios, android or all");
|
|
29964
30214
|
log.step(`Exporting ${platforms.join(" and ")} bundles`);
|
|
29965
30215
|
const { out, bundles } = await exportBundles(platforms);
|
|
@@ -29981,13 +30231,13 @@ async function publish(opts) {
|
|
|
29981
30231
|
for (const m2 of prep.missing) {
|
|
29982
30232
|
const f = bySha.get(m2.sha256);
|
|
29983
30233
|
spin.message(`Uploading ${++done}/${prep.missing.length}: ${f.path}`);
|
|
29984
|
-
const res = await fetch(m2.uploadUrl, { method: "PUT", body:
|
|
30234
|
+
const res = await fetch(m2.uploadUrl, { method: "PUT", body: readFileSync10(f.abs), headers: { "Content-Type": f.contentType } });
|
|
29985
30235
|
if (!res.ok) fail(`Upload failed for ${f.path} (${res.status})`);
|
|
29986
30236
|
}
|
|
29987
30237
|
spin.stop(`${all.length} files: ${prep.missing.length} uploaded, ${prep.alreadyStored} already stored`);
|
|
29988
30238
|
const key = loadPrivateKey(opts.privateKey);
|
|
29989
30239
|
const published = [];
|
|
29990
|
-
if (key) log.info(`Signing with ${
|
|
30240
|
+
if (key) log.info(`Signing with ${import_picocolors13.default.cyan(key.source)}`);
|
|
29991
30241
|
for (const [runtime, group] of groups) {
|
|
29992
30242
|
const body = {
|
|
29993
30243
|
message,
|
|
@@ -30013,11 +30263,11 @@ async function publish(opts) {
|
|
|
30013
30263
|
if (notice) log.warn(notice);
|
|
30014
30264
|
published.push({ id: release.id, channel, runtimeVersion: runtime, platforms: group, rolloutPercent: release.rolloutPercent, signed: release.signed, message, notice });
|
|
30015
30265
|
log.success(
|
|
30016
|
-
`Published to ${
|
|
30266
|
+
`Published to ${import_picocolors13.default.bold(channel)} \xB7 runtime ${runtime} \xB7 ${group.join(", ")}${release.rolloutPercent < 100 ? ` \xB7 ${release.rolloutPercent}% rollout` : ""}${release.signed ? " \xB7 signed" : ""}`
|
|
30017
30267
|
);
|
|
30018
30268
|
}
|
|
30019
30269
|
const url = `${dashboardUrl()}/${config.org}/${config.project}/updates/${channel}`;
|
|
30020
|
-
outro(
|
|
30270
|
+
outro(import_picocolors13.default.dim(url));
|
|
30021
30271
|
return { releases: published, url };
|
|
30022
30272
|
} finally {
|
|
30023
30273
|
rmSync4(out, { recursive: true, force: true });
|
|
@@ -30029,8 +30279,8 @@ async function list(opts) {
|
|
|
30029
30279
|
const releases = await unwrap(client().GET("/v1/orgs/{org}/projects/{project}/channels/{channel}/releases", { params: { path: { org: config.org, project: config.project, channel } } }));
|
|
30030
30280
|
if (!releases.length) console.log(`No releases on ${channel} yet.`);
|
|
30031
30281
|
for (const r2 of releases.slice(0, 20)) {
|
|
30032
|
-
const status = r2.status === "active" ?
|
|
30033
|
-
console.log(`${new Date(r2.createdAt).toLocaleString().padEnd(24)} ${status.padEnd(22)} ${String(r2.rolloutPercent).padStart(3)}% ${
|
|
30282
|
+
const status = r2.status === "active" ? import_picocolors13.default.green("active") : r2.status === "rolled_back" ? import_picocolors13.default.red("rolled back") : import_picocolors13.default.dim("superseded");
|
|
30283
|
+
console.log(`${new Date(r2.createdAt).toLocaleString().padEnd(24)} ${status.padEnd(22)} ${String(r2.rolloutPercent).padStart(3)}% ${import_picocolors13.default.dim(r2.runtimeVersion.padEnd(10))} ${r2.message}`);
|
|
30034
30284
|
}
|
|
30035
30285
|
return { channel, releases: releases.slice(0, 20) };
|
|
30036
30286
|
}
|
|
@@ -30045,9 +30295,9 @@ async function rollback(opts) {
|
|
|
30045
30295
|
const { path: path2, active } = await activeRelease(opts.channel ?? "production");
|
|
30046
30296
|
const request = client().POST("/v1/orgs/{org}/projects/{project}/channels/{channel}/releases/{release}/rollback", { params: { path: { ...path2, release: active.id } } });
|
|
30047
30297
|
const res = await unwrap(request);
|
|
30048
|
-
console.log(`${
|
|
30298
|
+
console.log(`${import_picocolors13.default.green("\u2714")} Rolled back "${active.message}". ${res.active ? "The previous release is active again." : "Devices return to the bundle built into the app."}`);
|
|
30049
30299
|
const notice = (await request).response.headers.get("Liftbay-Notice");
|
|
30050
|
-
if (notice) console.log(`${
|
|
30300
|
+
if (notice) console.log(`${import_picocolors13.default.yellow("!")} ${notice}`);
|
|
30051
30301
|
return { rolledBack: { id: active.id, message: active.message }, active: res.active, notice };
|
|
30052
30302
|
}
|
|
30053
30303
|
async function rollout(percent, opts) {
|
|
@@ -30055,18 +30305,18 @@ async function rollout(percent, opts) {
|
|
|
30055
30305
|
const r2 = await unwrap(
|
|
30056
30306
|
client().PATCH("/v1/orgs/{org}/projects/{project}/channels/{channel}/releases/{release}/rollout", { params: { path: { ...path2, release: active.id } }, body: { rolloutPercent: Number(percent) } })
|
|
30057
30307
|
);
|
|
30058
|
-
console.log(`${
|
|
30308
|
+
console.log(`${import_picocolors13.default.green("\u2714")} "${r2.message}" now rolls out to ${r2.rolloutPercent}%`);
|
|
30059
30309
|
return { id: r2.id, message: r2.message, rolloutPercent: r2.rolloutPercent };
|
|
30060
30310
|
}
|
|
30061
30311
|
async function channels() {
|
|
30062
30312
|
const config = requireConfig();
|
|
30063
30313
|
const list2 = await unwrap(client().GET("/v1/orgs/{org}/projects/{project}/channels", { params: { path: { org: config.org, project: config.project } } }));
|
|
30064
|
-
for (const c2 of list2) console.log(`${
|
|
30314
|
+
for (const c2 of list2) console.log(`${import_picocolors13.default.bold(c2.name.padEnd(16))} env ${c2.environmentSlug.padEnd(12)} ${c2.protocols.join(", ").padEnd(32)} ${import_picocolors13.default.dim(c2.currentRelease?.message ?? "no active release")}`);
|
|
30065
30315
|
return { channels: list2 };
|
|
30066
30316
|
}
|
|
30067
30317
|
|
|
30068
30318
|
// src/index.ts
|
|
30069
|
-
var program2 = new Command("liftbay").description("Build, ship and update React Native apps").version("0.
|
|
30319
|
+
var program2 = new Command("liftbay").description("Build, ship and update React Native apps").version("0.3.1").option("--json", "print the result as JSON on stdout (progress goes to stderr; never prompts). Also LIFTBAY_JSON=1").hook("preAction", () => {
|
|
30070
30320
|
if (program2.opts().json || process.env.LIFTBAY_JSON === "1") enableJson();
|
|
30071
30321
|
});
|
|
30072
30322
|
var wrap = (fn) => async (...args) => {
|
|
@@ -30076,7 +30326,7 @@ var wrap = (fn) => async (...args) => {
|
|
|
30076
30326
|
reportError(e);
|
|
30077
30327
|
}
|
|
30078
30328
|
};
|
|
30079
|
-
var setupOptions = (cmd) => cmd.option("--org <slug>", "organization").option("--project <slug>", "project slug (created if it doesn't exist)").option("--platforms <list>", "ios, android or all").option("--runtime-policy <policy>", "appVersion or fingerprint").option("-y, --yes", "accept defaults, no prompts").option("--api-url <url>", "API URL (for self-hosted or local development)").option("--dashboard-url <url>", "dashboard URL");
|
|
30329
|
+
var setupOptions = (cmd) => cmd.option("--org <slug>", "organization").option("--project <slug>", "project slug (created if it doesn't exist)").option("--platforms <list>", "ios, android or all").option("--runtime-policy <policy>", "appVersion or fingerprint").option("--no-eas", "don't use eas.json's build profiles and env, even if there is one").option("-y, --yes", "accept defaults, no prompts").option("--api-url <url>", "API URL (for self-hosted or local development)").option("--dashboard-url <url>", "dashboard URL");
|
|
30080
30330
|
program2.command("login").description("Sign in through the browser").option("--api-url <url>", "API URL").option("--dashboard-url <url>", "dashboard URL").option("--no-open", "don't open the browser").option("--no-wait", "start the sign-in and exit; run `liftbay login` again after approving to finish (for agents)").action(wrap(login));
|
|
30081
30331
|
program2.command("logout").description("Sign out and remove the saved token").action(wrap(logout));
|
|
30082
30332
|
program2.command("whoami").description("Show the signed-in user and organizations").action(wrap(whoami));
|
|
@@ -30108,6 +30358,12 @@ var device = program2.command("device").description("iOS test devices for intern
|
|
|
30108
30358
|
device.command("add").description("Register an iPhone or iPad: shows a QR code to open on it, or pass --udid").option("--udid <udid>", "add by UDID instead of a registration link").option("--name <name>", "device name (with --udid)").option("--team-id <id>", "Apple team, when the organization has several").option("--no-wait", "print the registration link and exit instead of waiting for the device (for agents)").action(wrap(deviceAdd));
|
|
30109
30359
|
device.command("list").description("List registered devices").action(wrap(deviceList));
|
|
30110
30360
|
device.command("remove <id>").description("Remove a device (disabled at Apple)").action(wrap(deviceRemove));
|
|
30361
|
+
var env = program2.command("env").description("Environment variables of the project's environments (development, preview, production, \u2026)");
|
|
30362
|
+
env.command("list").description("Environments, or with --environment its variables (sensitive values masked, secrets never shown)").option("-e, --environment <slug>", "environment").action(wrap(envList));
|
|
30363
|
+
env.command("import <files...>").description("Import .env files (into the environment each is named for, e.g. .env.production, or --environment), or eas.json (every build profile's env)").option("-e, --environment <slug>", "environment (for eas.json: only that profile)").option("--visibility <visibility>", "plain, sensitive or secret for all keys (default: EXPO_PUBLIC_* plain, *_TOKEN/*_SECRET/\u2026 secret, the rest sensitive)").option("--no-overwrite", "keep values that are already set").action(wrap(envImport));
|
|
30364
|
+
env.command("set <pairs...>").description("Set KEY=value pairs (values end up in shell history: prefer `env import` for secrets)").option("-e, --environment <slug>", "environment").option("--visibility <visibility>", "plain, sensitive or secret (default: from the name)").action(wrap(envSet));
|
|
30365
|
+
env.command("unset <keys...>").description("Remove variables").option("-e, --environment <slug>", "environment").action(wrap(envUnset));
|
|
30366
|
+
env.command("export").description("Print an environment as a .env file, or write it with --out (secret values are never exported)").option("-e, --environment <slug>", "environment").option("--out <file>", "file to write").option("--force", "overwrite the file").action(wrap(envExport));
|
|
30111
30367
|
var ota = program2.command("ota").description("Over-the-air updates");
|
|
30112
30368
|
setupOptions(ota.command("init").description("Set up expo-updates, the Liftbay plugin and channels")).action(wrap(otaInitCommand));
|
|
30113
30369
|
var update = program2.command("update").description("Publish and manage OTA updates");
|
package/package.json
CHANGED
package/skills/liftbay/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: liftbay
|
|
3
|
-
description: Build, sign, submit and update React Native and Expo apps with Liftbay (liftbay.dev). Use when the user wants to set up CI/CD for a React Native or Expo app, build an iOS .ipa or Android .apk/.aab, set up iOS or Android signing, upload to TestFlight or Google Play, register test iPhones for ad hoc installs, or publish, roll out or roll back over-the-air (OTA) updates. Also use when a repo has a liftbay.json file or the user mentions Liftbay.
|
|
3
|
+
description: Build, sign, submit and update React Native and Expo apps with Liftbay (liftbay.dev). Use when the user wants to set up CI/CD for a React Native or Expo app, move an app from EAS (eas.json) to Liftbay, manage build environment variables or .env files, build an iOS .ipa or Android .apk/.aab, set up iOS or Android signing, upload to TestFlight or Google Play, register test iPhones for ad hoc installs, or publish, roll out or roll back over-the-air (OTA) updates. Also use when a repo has a liftbay.json file or the user mentions Liftbay.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Liftbay
|
|
@@ -14,7 +14,7 @@ You drive it through the `liftbay` CLI. Run it as `npx liftbay@latest <command>`
|
|
|
14
14
|
- **Always pass `--json`.** Stdout then holds exactly one JSON document: the result, or `{"error": {"code", "message", ...}}` with exit code 1. Progress and logs go to stderr. With `--json` the CLI never prompts; when it needs something, it fails with a message naming the flag to pass.
|
|
15
15
|
- **Never block on long waits.** Use `--no-wait` on `build`, `submit`, `login` and `device add`, then check back with `builds view`, `submissions list` or `whoami`.
|
|
16
16
|
- **Ask the user before anything that reaches real users or stores:** an OTA publish, rollout change or rollback on `production`; submitting to TestFlight; any Google Play track other than `internal`; cancelling a build someone else started.
|
|
17
|
-
- **Never handle secrets yourself.** Don't ask for, print, pass or commit Apple passwords, keystore passwords or the update signing key (`keys/liftbay-private-key.pem`). When a step needs one, ask the user to run that command in their own terminal.
|
|
17
|
+
- **Never handle secrets yourself.** Importing `.env` files the user points you to is fine; never print their values. Don't ask for, print, pass or commit Apple passwords, keystore passwords or the update signing key (`keys/liftbay-private-key.pem`). When a step needs one, ask the user to run that command in their own terminal.
|
|
18
18
|
- Read error messages: they say exactly what to do next.
|
|
19
19
|
|
|
20
20
|
## 1. Sign in
|
|
@@ -49,8 +49,10 @@ liftbay init --yes --json
|
|
|
49
49
|
|
|
50
50
|
This detects the app (Expo or bare React Native), creates or links the Liftbay project, and writes `liftbay.json`. It also writes `.github/workflows/liftbay.yml` and sets up OTA updates with expo-updates and `@liftbay/expo-plugin` in `app.json`.
|
|
51
51
|
|
|
52
|
+
**Coming from EAS:** when the app has an `eas.json`, `init` uses it. Each EAS build profile becomes a Liftbay build profile with the same name, channel and distribution, and its `env` (with `extends` resolved) goes into an environment of the same name. Pass `--no-eas` to use Liftbay's default profiles instead.
|
|
53
|
+
|
|
52
54
|
Useful flags:
|
|
53
|
-
- `--org <slug>`: required if the user is in several organizations; the error lists them.
|
|
55
|
+
- `--org <slug>`: required if the user is in several organizations; the error lists them. For a new account with no organization yet, `init` creates one, named after `--org` or the person's name.
|
|
54
56
|
- `--project <slug>`
|
|
55
57
|
- `--platforms ios|android|all`
|
|
56
58
|
- `--runtime-policy appVersion|fingerprint`
|
|
@@ -72,7 +74,37 @@ Then:
|
|
|
72
74
|
|
|
73
75
|
Each profile has an `environment` (its secrets and variables) and an OTA `channel` with the same name.
|
|
74
76
|
|
|
75
|
-
## 3.
|
|
77
|
+
## 3. Environment variables
|
|
78
|
+
|
|
79
|
+
Builds get the variables of their profile's environment, as real environment variables and in `.env` for Expo. Set them before the first build, or the app will build without its API URLs and keys.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
liftbay env list --json # environments and how many variables each has
|
|
83
|
+
liftbay env list -e production --json # one environment's variables
|
|
84
|
+
liftbay env import .env.production .env.preview --json # each file into the environment it's named for
|
|
85
|
+
liftbay env import .env.staging -e preview --json # any file into a named environment
|
|
86
|
+
liftbay env import eas.json --json # every EAS profile's env (overwrites)
|
|
87
|
+
liftbay env set API_REGION=us -e production --json
|
|
88
|
+
liftbay env unset OLD_KEY -e production --json
|
|
89
|
+
liftbay env export -e production --out .env.production.liftbay --json
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
When the user gives you `.env` files or paths, import them. Find them with `ls -a`: `.env`, `.env.local`, `.env.development`, `.env.production` and so on.
|
|
93
|
+
- Map a file to an environment by its name. Ask when it's unclear: `.env` and `.env.local` are often local development settings, not production.
|
|
94
|
+
- Don't print values or read secrets aloud. Report key names and counts only.
|
|
95
|
+
|
|
96
|
+
Visibility is chosen per key:
|
|
97
|
+
- `EXPO_PUBLIC_*` → plain, because it's inlined into the app bundle.
|
|
98
|
+
- Names like `*_TOKEN`, `*_SECRET`, `*_PASSWORD`, `*_KEY` → secret (write-only).
|
|
99
|
+
- Everything else → sensitive (encrypted, revealable).
|
|
100
|
+
|
|
101
|
+
Use `--visibility` to override this for a whole import.
|
|
102
|
+
|
|
103
|
+
If the app used EAS environment variables stored on expo.dev (`eas env:*`), they aren't in `eas.json`. Pull them first with `npx eas env:pull --environment production --path .env.eas.production`, then `liftbay env import .env.eas.production -e production`.
|
|
104
|
+
|
|
105
|
+
A common build failure after moving from EAS: with `SENTRY_ORG`/`SENTRY_PROJECT` set, the Sentry plugin uploads source maps and needs `SENTRY_AUTH_TOKEN`. The user adds it as a secret (`liftbay env import` from a file, or the dashboard), or removes the two Sentry variables.
|
|
106
|
+
|
|
107
|
+
## 4. Signing
|
|
76
108
|
|
|
77
109
|
**Android:** nothing to do. Liftbay creates the upload key on the first Android build and stores it encrypted.
|
|
78
110
|
- To bring an existing key, the user runs `liftbay credentials android import --keystore <file> --alias <alias>`, with the password in `LIFTBAY_KEYSTORE_PASSWORD`.
|
|
@@ -94,7 +126,7 @@ Check what's stored:
|
|
|
94
126
|
liftbay credentials --json
|
|
95
127
|
```
|
|
96
128
|
|
|
97
|
-
##
|
|
129
|
+
## 5. Build
|
|
98
130
|
|
|
99
131
|
```bash
|
|
100
132
|
liftbay build --platform android --profile preview --no-wait --json
|
|
@@ -121,7 +153,7 @@ Other flags:
|
|
|
121
153
|
|
|
122
154
|
**Xcode Cloud (optional):** `liftbay xcode-cloud connect --json`. Its error says which one-time step the user has to do in App Store Connect.
|
|
123
155
|
|
|
124
|
-
##
|
|
156
|
+
## 6. Submit to the stores
|
|
125
157
|
|
|
126
158
|
Submit as part of the build:
|
|
127
159
|
|
|
@@ -142,7 +174,7 @@ liftbay submissions view <id> --json
|
|
|
142
174
|
- Play tracks are `internal`, `alpha`, `beta` and `production`. Use `--rollout <percent>` for a staged rollout.
|
|
143
175
|
- The first upload of a brand-new Play app must be done by hand in Play Console. Until then, uploads are saved as drafts.
|
|
144
176
|
|
|
145
|
-
##
|
|
177
|
+
## 7. Ad hoc iOS installs on test devices
|
|
146
178
|
|
|
147
179
|
1. Register the device:
|
|
148
180
|
```bash
|
|
@@ -160,7 +192,7 @@ liftbay submissions view <id> --json
|
|
|
160
192
|
```
|
|
161
193
|
4. When the build has succeeded, `liftbay builds view <n> --json` returns `installUrl`. The user opens it on the registered device.
|
|
162
194
|
|
|
163
|
-
##
|
|
195
|
+
## 8. OTA updates (JS-only changes, no store review)
|
|
164
196
|
|
|
165
197
|
The app must have been built once after `liftbay ota init`, so the update URL is inside the binary. Then publish:
|
|
166
198
|
|
|
@@ -199,7 +231,9 @@ An update only reaches binaries with the same runtime version. When native code
|
|
|
199
231
|
| Build fails at once: no GitHub repository | Push the app to GitHub, then `liftbay ci init --yes --json` |
|
|
200
232
|
| Build fails: workflow not found / 404 | Commit and push `.github/workflows/liftbay.yml` on the branch you build |
|
|
201
233
|
| `github_not_connected` | User installs the Liftbay GitHub App at `liftbay.dev/<org>/github` |
|
|
202
|
-
| iOS build is a simulator build | No iOS signing yet: section
|
|
234
|
+
| iOS build is a simulator build | No iOS signing yet: section 4 |
|
|
235
|
+
| App builds but can't reach its backend | Missing env variables: `liftbay env list -e <env>`, then section 3 |
|
|
236
|
+
| `Pass --environment` | The file name doesn't match an environment; pass `-e <slug>` |
|
|
203
237
|
| `Apple ID sign-in needs a person` / two-factor code | Ask the user to run `npx liftbay credentials ios` in their terminal |
|
|
204
238
|
| `certificate_limit` | Apple allows a few distribution certificates per team. The user runs `liftbay credentials ios` interactively to pick one to revoke |
|
|
205
239
|
| `no_store_build` | Build with `--profile production` first |
|