liftbay 0.5.2 → 0.7.0
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/dist/index.js +888 -544
- package/package.json +2 -2
- package/skills/liftbay/SKILL.md +39 -17
package/dist/index.js
CHANGED
|
@@ -26526,6 +26526,26 @@ function emit(result) {
|
|
|
26526
26526
|
var cliName = () => process.env.LIFTBAY_CLI_NAME ?? (/[\\/]_npx[\\/]/.test(process.argv[1] ?? "") || process.env.npm_command === "exec" ? "npx liftbay" : "liftbay");
|
|
26527
26527
|
var cmd = (command) => command.replace(/^liftbay\b/, cliName());
|
|
26528
26528
|
var withCliName = (text2) => cliName() === "liftbay" ? text2 : text2.replace(/`liftbay /g, `\`${cliName()} `);
|
|
26529
|
+
function isTransient(e) {
|
|
26530
|
+
if (e instanceof LiftbayApiError) return e.status >= 500 || e.status === 429;
|
|
26531
|
+
const err = e;
|
|
26532
|
+
const code = err?.cause?.code ?? err?.code ?? "";
|
|
26533
|
+
return err?.name === "TypeError" && err.message === "fetch failed" || /^(ECONN|EADDR|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|EPIPE|ENETUNREACH|EHOSTUNREACH|UND_ERR)/.test(code);
|
|
26534
|
+
}
|
|
26535
|
+
async function poll(fn, onRetry) {
|
|
26536
|
+
const until = Date.now() + 5 * 6e4;
|
|
26537
|
+
for (let attempt = 1; ; attempt++) {
|
|
26538
|
+
try {
|
|
26539
|
+
return await fn();
|
|
26540
|
+
} catch (e) {
|
|
26541
|
+
if (!isTransient(e) || Date.now() > until) throw e;
|
|
26542
|
+
if (attempt === 1) process.stderr.write(`${import_picocolors.default.yellow("!")} Connection to Liftbay lost; retrying\u2026
|
|
26543
|
+
`);
|
|
26544
|
+
onRetry?.(attempt);
|
|
26545
|
+
await new Promise((r2) => setTimeout(r2, Math.min(15e3, 1e3 * 2 ** Math.min(attempt, 4))));
|
|
26546
|
+
}
|
|
26547
|
+
}
|
|
26548
|
+
}
|
|
26529
26549
|
function reportError(e) {
|
|
26530
26550
|
if (json) {
|
|
26531
26551
|
const error = e instanceof LiftbayApiError ? { code: e.code, message: e.message, status: e.status, fields: e.fields } : e instanceof CliError ? { code: "cli_error", message: e.message } : { code: "internal", message: e instanceof Error ? e.message : String(e) };
|
|
@@ -26541,15 +26561,19 @@ function reportError(e) {
|
|
|
26541
26561
|
} else console.error(e);
|
|
26542
26562
|
process.exit(1);
|
|
26543
26563
|
}
|
|
26544
|
-
var
|
|
26564
|
+
var promptsOff = false;
|
|
26565
|
+
function disablePrompts() {
|
|
26566
|
+
promptsOff = true;
|
|
26567
|
+
}
|
|
26568
|
+
var isInteractive = () => !json && !promptsOff && !!process.stdin.isTTY && !!process.stdout.isTTY && !process.env.CI;
|
|
26545
26569
|
function needInteractive(instead) {
|
|
26546
26570
|
if (!isInteractive()) fail(instead);
|
|
26547
26571
|
}
|
|
26548
26572
|
|
|
26549
26573
|
// src/commands/auth.ts
|
|
26550
|
-
import { spawn } from "child_process";
|
|
26551
|
-
import { mkdirSync as mkdirSync2, readFileSync as
|
|
26552
|
-
import { join as
|
|
26574
|
+
import { spawn as spawn2 } from "child_process";
|
|
26575
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
26576
|
+
import { join as join4 } from "path";
|
|
26553
26577
|
|
|
26554
26578
|
// node_modules/@clack/core/dist/index.mjs
|
|
26555
26579
|
import { styleText } from "util";
|
|
@@ -27886,29 +27910,279 @@ ${i2}
|
|
|
27886
27910
|
}).prompt();
|
|
27887
27911
|
|
|
27888
27912
|
// src/commands/auth.ts
|
|
27913
|
+
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
27914
|
+
|
|
27915
|
+
// src/commands/link.ts
|
|
27889
27916
|
var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
27917
|
+
|
|
27918
|
+
// src/project.ts
|
|
27919
|
+
import { execFile, spawn } from "child_process";
|
|
27920
|
+
import { promisify } from "util";
|
|
27921
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
27922
|
+
import { join as join3 } from "path";
|
|
27923
|
+
var execFileAsync = promisify(execFile);
|
|
27924
|
+
var CONFIG_FILE = "liftbay.json";
|
|
27925
|
+
function readConfig(cwd = process.cwd()) {
|
|
27926
|
+
const path2 = join3(cwd, CONFIG_FILE);
|
|
27927
|
+
return existsSync3(path2) ? JSON.parse(readFileSync2(path2, "utf8")) : null;
|
|
27928
|
+
}
|
|
27929
|
+
function requireConfig(cwd = process.cwd()) {
|
|
27930
|
+
return readConfig(cwd) ?? fail(`No ${CONFIG_FILE} here. Run \`liftbay init\` in your app's folder.`);
|
|
27931
|
+
}
|
|
27932
|
+
function writeConfig(config, cwd = process.cwd()) {
|
|
27933
|
+
writeFileSync2(join3(cwd, CONFIG_FILE), `${JSON.stringify({ $schema: "https://liftbay.dev/schema/liftbay.json", ...config }, null, 2)}
|
|
27934
|
+
`);
|
|
27935
|
+
}
|
|
27936
|
+
function readJson(path2) {
|
|
27937
|
+
return existsSync3(path2) ? JSON.parse(readFileSync2(path2, "utf8")) : null;
|
|
27938
|
+
}
|
|
27939
|
+
function packageManager(cwd = process.cwd()) {
|
|
27940
|
+
if (existsSync3(join3(cwd, "bun.lock")) || existsSync3(join3(cwd, "bun.lockb"))) return "bun";
|
|
27941
|
+
if (existsSync3(join3(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
27942
|
+
if (existsSync3(join3(cwd, "yarn.lock"))) return "yarn";
|
|
27943
|
+
return "npm";
|
|
27944
|
+
}
|
|
27945
|
+
async function output(cmd2, args, cwd = process.cwd()) {
|
|
27946
|
+
const { stdout: stdout2 } = await execFileAsync(cmd2, args, { cwd, encoding: "utf8", maxBuffer: 50 * 1024 * 1024 });
|
|
27947
|
+
return stdout2;
|
|
27948
|
+
}
|
|
27949
|
+
async function expoConfig(cwd = process.cwd()) {
|
|
27950
|
+
return JSON.parse(await output("npx", ["expo", "config", "--json", "--type", "public"], cwd));
|
|
27951
|
+
}
|
|
27952
|
+
async function detectApp(cwd = process.cwd()) {
|
|
27953
|
+
const pkg = readJson(join3(cwd, "package.json"));
|
|
27954
|
+
if (!pkg) fail("No package.json here. Run this in your React Native app's folder.");
|
|
27955
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
27956
|
+
if (!deps["react-native"]) fail("This doesn't look like a React Native app (react-native is not a dependency).");
|
|
27957
|
+
const hasExpo = !!deps.expo;
|
|
27958
|
+
let name = pkg.name ?? "app";
|
|
27959
|
+
let slug = name;
|
|
27960
|
+
let version = pkg.version ?? null;
|
|
27961
|
+
let bundleId = null;
|
|
27962
|
+
let androidPackage = null;
|
|
27963
|
+
if (hasExpo) {
|
|
27964
|
+
const cfg = await expoConfig(cwd);
|
|
27965
|
+
name = cfg.name ?? name;
|
|
27966
|
+
slug = cfg.slug ?? slug;
|
|
27967
|
+
version = cfg.version ?? version;
|
|
27968
|
+
bundleId = cfg.ios?.bundleIdentifier ?? null;
|
|
27969
|
+
androidPackage = cfg.android?.package ?? null;
|
|
27970
|
+
} else {
|
|
27971
|
+
const gradle = join3(cwd, "android/app/build.gradle");
|
|
27972
|
+
if (existsSync3(gradle)) androidPackage = readFileSync2(gradle, "utf8").match(/applicationId\s+["']([^"']+)["']/)?.[1] ?? null;
|
|
27973
|
+
}
|
|
27974
|
+
return {
|
|
27975
|
+
framework: hasExpo ? "expo" : "react-native",
|
|
27976
|
+
name,
|
|
27977
|
+
slug: String(slug).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "app",
|
|
27978
|
+
version,
|
|
27979
|
+
bundleId,
|
|
27980
|
+
androidPackage,
|
|
27981
|
+
hasExpoModules: hasExpo,
|
|
27982
|
+
hasExpoUpdates: !!deps["expo-updates"],
|
|
27983
|
+
packageManager: packageManager(cwd),
|
|
27984
|
+
appJson: existsSync3(join3(cwd, "app.json"))
|
|
27985
|
+
};
|
|
27986
|
+
}
|
|
27987
|
+
async function run(cmd2, args, cwd = process.cwd(), env2 = {}) {
|
|
27988
|
+
const code = await new Promise((resolve4, reject) => {
|
|
27989
|
+
const child = spawn(cmd2, args, { cwd, stdio: "inherit", env: { ...process.env, ...env2 } });
|
|
27990
|
+
child.on("error", reject);
|
|
27991
|
+
child.on("close", resolve4);
|
|
27992
|
+
});
|
|
27993
|
+
if (code !== 0) fail(`\`${cmd2} ${args.join(" ")}\` failed (exit ${code})`);
|
|
27994
|
+
}
|
|
27995
|
+
async function runQuiet(label, cmd2, args, cwd = process.cwd()) {
|
|
27996
|
+
const spin = spinner();
|
|
27997
|
+
spin.start(label);
|
|
27998
|
+
const lines = [];
|
|
27999
|
+
const code = await new Promise((resolve4, reject) => {
|
|
28000
|
+
const child = spawn(cmd2, args, {
|
|
28001
|
+
cwd,
|
|
28002
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
28003
|
+
env: { ...process.env, npm_config_audit: "false", npm_config_fund: "false", npm_config_update_notifier: "false" }
|
|
28004
|
+
});
|
|
28005
|
+
const keep = (chunk) => lines.push(...chunk.toString().split(/\r?\n/).filter(Boolean));
|
|
28006
|
+
child.stdout.on("data", keep);
|
|
28007
|
+
child.stderr.on("data", keep);
|
|
28008
|
+
child.on("error", reject);
|
|
28009
|
+
child.on("close", resolve4);
|
|
28010
|
+
});
|
|
28011
|
+
if (code === 0) return spin.stop(label.replace(/^Installing/, "Installed").replace(/^Setting up/, "Set up"));
|
|
28012
|
+
spin.error(`${label} failed`);
|
|
28013
|
+
console.error(lines.slice(-40).join("\n"));
|
|
28014
|
+
fail(`\`${cmd2} ${args.join(" ")}\` failed (exit ${code})`);
|
|
28015
|
+
}
|
|
28016
|
+
function readAppJson(cwd = process.cwd()) {
|
|
28017
|
+
return readJson(join3(cwd, "app.json"));
|
|
28018
|
+
}
|
|
28019
|
+
function writeAppJson(data, cwd = process.cwd()) {
|
|
28020
|
+
writeFileSync2(join3(cwd, "app.json"), `${JSON.stringify(data, null, 2)}
|
|
28021
|
+
`);
|
|
28022
|
+
}
|
|
28023
|
+
|
|
28024
|
+
// src/commands/link.ts
|
|
28025
|
+
function cancelled(value) {
|
|
28026
|
+
if (isCancel(value)) {
|
|
28027
|
+
cancel("Cancelled");
|
|
28028
|
+
process.exit(0);
|
|
28029
|
+
}
|
|
28030
|
+
return value;
|
|
28031
|
+
}
|
|
28032
|
+
var slugifyProject = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
|
|
28033
|
+
var slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
28034
|
+
async function createFirstOrg(lb, given) {
|
|
28035
|
+
const me = await unwrap(lb.GET("/v1/auth/me"));
|
|
28036
|
+
const suggested = given ?? (me.user.name?.trim() || me.user.email.split("@")[0]);
|
|
28037
|
+
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;
|
|
28038
|
+
const base = slugify(name).padEnd(2, "0");
|
|
28039
|
+
for (const slug of [base, `${base.slice(0, 35)}-${Math.random().toString(36).slice(2, 6)}`]) {
|
|
28040
|
+
const res = await lb.POST("/v1/orgs", { body: { name: name.slice(0, 64), slug } });
|
|
28041
|
+
if (res.data) {
|
|
28042
|
+
log.success(`Created organization ${import_picocolors2.default.bold(res.data.name)} ${import_picocolors2.default.dim(`(liftbay.dev/${res.data.slug})`)}`);
|
|
28043
|
+
return res.data;
|
|
28044
|
+
}
|
|
28045
|
+
if (res.response.status !== 409) await unwrap(Promise.resolve(res));
|
|
28046
|
+
}
|
|
28047
|
+
return fail("Couldn't pick a free organization URL. Pass --org <slug>.");
|
|
28048
|
+
}
|
|
28049
|
+
async function ensureLinked(app, opts) {
|
|
28050
|
+
const existing = readConfig();
|
|
28051
|
+
if (existing) {
|
|
28052
|
+
log.success(`Linked to ${import_picocolors2.default.bold(`${existing.org}/${existing.project}`)}`);
|
|
28053
|
+
return existing;
|
|
28054
|
+
}
|
|
28055
|
+
const lb = client();
|
|
28056
|
+
const orgs = await unwrap(lb.GET("/v1/orgs"));
|
|
28057
|
+
let org;
|
|
28058
|
+
if (opts.newOrg || !orgs.length) {
|
|
28059
|
+
const created = await createFirstOrg(lb, opts.newOrg ?? opts.org);
|
|
28060
|
+
orgs.push(created);
|
|
28061
|
+
org = created.slug;
|
|
28062
|
+
}
|
|
28063
|
+
org ??= opts.org ?? (orgs.length === 1 ? orgs[0].slug : void 0);
|
|
28064
|
+
if (opts.org && !orgs.some((o) => o.slug === org)) fail(`You're not in an organization "${opts.org}" (${orgs.map((o) => o.slug).join(", ")}). To create one, pass --new-org "<name>".`);
|
|
28065
|
+
if (!org) {
|
|
28066
|
+
if (!isInteractive()) fail(`Several organizations available; pass --org (${orgs.map((o) => o.slug).join(", ")})`);
|
|
28067
|
+
org = cancelled(await select({ message: "Organization", options: orgs.map((o) => ({ value: o.slug, label: o.name, hint: o.slug })) }));
|
|
28068
|
+
}
|
|
28069
|
+
const projects = await unwrap(lb.GET("/v1/orgs/{org}/projects", { params: { path: { org } } }));
|
|
28070
|
+
let projectName = opts.projectName;
|
|
28071
|
+
let slug = opts.project ?? (projectName ? slugifyProject(projectName) : void 0);
|
|
28072
|
+
if (!slug) {
|
|
28073
|
+
const match = projects.find((pr) => pr.slug === app.slug);
|
|
28074
|
+
if (opts.yes || !isInteractive()) slug = match?.slug ?? app.slug;
|
|
28075
|
+
else {
|
|
28076
|
+
const taken = projects.some((pr) => pr.slug === app.slug);
|
|
28077
|
+
const choice = cancelled(
|
|
28078
|
+
await select({
|
|
28079
|
+
message: "Liftbay project",
|
|
28080
|
+
options: [
|
|
28081
|
+
...taken ? [] : [{ value: "__new__", label: `Create "${app.name}"`, hint: `new project \xB7 liftbay.dev/${org}/${app.slug}` }],
|
|
28082
|
+
{ value: "__named__", label: "Create a project with another name" },
|
|
28083
|
+
...projects.map((pr) => ({ value: pr.slug, label: pr.name, hint: `existing \xB7 ${pr.slug}` }))
|
|
28084
|
+
],
|
|
28085
|
+
initialValue: match?.slug ?? (taken ? "__named__" : "__new__")
|
|
28086
|
+
})
|
|
28087
|
+
);
|
|
28088
|
+
if (choice === "__named__") {
|
|
28089
|
+
projectName = cancelled(
|
|
28090
|
+
await text({
|
|
28091
|
+
message: "Project name",
|
|
28092
|
+
initialValue: app.name,
|
|
28093
|
+
validate: (v) => {
|
|
28094
|
+
const s = slugifyProject(v ?? "");
|
|
28095
|
+
if (s.length < 2) return "At least 2 letters or digits";
|
|
28096
|
+
if (projects.some((pr) => pr.slug === s)) return `${s} already exists: pick it from the list instead`;
|
|
28097
|
+
}
|
|
28098
|
+
})
|
|
28099
|
+
);
|
|
28100
|
+
slug = slugifyProject(projectName);
|
|
28101
|
+
} else slug = choice === "__new__" ? app.slug : choice;
|
|
28102
|
+
}
|
|
28103
|
+
}
|
|
28104
|
+
let project2 = projects.find((pr) => pr.slug === slug);
|
|
28105
|
+
if (!project2) {
|
|
28106
|
+
project2 = await unwrap(
|
|
28107
|
+
lb.POST("/v1/orgs/{org}/projects", {
|
|
28108
|
+
params: { path: { org } },
|
|
28109
|
+
body: { name: projectName ?? app.name, slug, framework: app.framework, bundleId: app.bundleId, androidPackage: app.androidPackage }
|
|
28110
|
+
})
|
|
28111
|
+
);
|
|
28112
|
+
log.success(`Created project ${import_picocolors2.default.bold(`${org}/${project2.slug}`)} with development, preview and production environments`);
|
|
28113
|
+
} else {
|
|
28114
|
+
log.success(`Linked to existing project ${import_picocolors2.default.bold(`${org}/${project2.slug}`)}`);
|
|
28115
|
+
}
|
|
28116
|
+
const config = { org, project: project2.slug, projectId: project2.id };
|
|
28117
|
+
writeConfig(config);
|
|
28118
|
+
log.info(`Wrote ${import_picocolors2.default.cyan(cmd("liftbay.json"))} ${import_picocolors2.default.dim("(commit it: it links this repository to the project)")}`);
|
|
28119
|
+
return config;
|
|
28120
|
+
}
|
|
28121
|
+
|
|
28122
|
+
// src/commands/auth.ts
|
|
27890
28123
|
var sleep = (ms) => new Promise((r2) => setTimeout(r2, ms));
|
|
27891
28124
|
function openBrowser(url) {
|
|
27892
28125
|
const cmd2 = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
27893
28126
|
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
27894
28127
|
try {
|
|
27895
|
-
|
|
28128
|
+
spawn2(cmd2, args, { stdio: "ignore", detached: true }).unref();
|
|
27896
28129
|
} catch {
|
|
27897
28130
|
}
|
|
27898
28131
|
}
|
|
27899
|
-
var PENDING =
|
|
28132
|
+
var PENDING = join4(CONFIG_DIR, "pending-login.json");
|
|
27900
28133
|
function readPending(api) {
|
|
27901
28134
|
try {
|
|
27902
|
-
const pending = JSON.parse(
|
|
28135
|
+
const pending = JSON.parse(readFileSync3(PENDING, "utf8"));
|
|
27903
28136
|
return pending.apiUrl === api && Date.parse(pending.expiresAt) > Date.now() + 5e3 ? pending : null;
|
|
27904
28137
|
} catch {
|
|
27905
28138
|
return null;
|
|
27906
28139
|
}
|
|
27907
28140
|
}
|
|
27908
28141
|
var clearPending = () => rmSync2(PENDING, { force: true });
|
|
28142
|
+
async function loginWithEmail(opts, api, dashboard) {
|
|
28143
|
+
const anon = createLiftbayClient({ baseUrl: api });
|
|
28144
|
+
const email = opts.email.trim();
|
|
28145
|
+
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) fail(`"${email}" isn't an email address`);
|
|
28146
|
+
let code = opts.code?.replace(/\s/g, "");
|
|
28147
|
+
if (!code) {
|
|
28148
|
+
await unwrap(anon.POST("/v1/auth/email/start", { body: { email } }));
|
|
28149
|
+
if (!isInteractive()) {
|
|
28150
|
+
log.info(`Sent a 6-digit sign-in code to ${import_picocolors3.default.bold(email)}.`);
|
|
28151
|
+
return {
|
|
28152
|
+
status: "code_sent",
|
|
28153
|
+
email,
|
|
28154
|
+
next: `Ask the person for the 6-digit code from the email, then run: liftbay login --email ${email} --code <code>`
|
|
28155
|
+
};
|
|
28156
|
+
}
|
|
28157
|
+
log.info(`Sent a 6-digit code to ${import_picocolors3.default.bold(email)}. ${import_picocolors3.default.dim("New to Liftbay? Signing in creates your account.")}`);
|
|
28158
|
+
code = cancelled(await text({ message: "Code from the email", validate: (v) => /^\d{6}$/.test((v ?? "").replace(/\s/g, "")) ? void 0 : "Enter the 6-digit code" })).replace(/\s/g, "");
|
|
28159
|
+
}
|
|
28160
|
+
const res = await unwrap(anon.POST("/v1/auth/email/token", { body: { email, code } }));
|
|
28161
|
+
clearPending();
|
|
28162
|
+
saveCredentials({ apiUrl: api, dashboardUrl: dashboard, token: res.token, email: res.user.email });
|
|
28163
|
+
log.success(`Signed in as ${import_picocolors3.default.bold(res.user.email)}`);
|
|
28164
|
+
return { status: "signed_in", user: res.user };
|
|
28165
|
+
}
|
|
27909
28166
|
async function login(opts) {
|
|
27910
28167
|
const api = opts.apiUrl ?? apiUrl();
|
|
27911
28168
|
const dashboard = opts.dashboardUrl ?? dashboardUrl();
|
|
28169
|
+
if (opts.code && !opts.email) fail("Pass --email with --code");
|
|
28170
|
+
if (opts.email) return loginWithEmail(opts, api, dashboard);
|
|
28171
|
+
if (isInteractive() && !readPending(api)) {
|
|
28172
|
+
const how = cancelled(
|
|
28173
|
+
await select({
|
|
28174
|
+
message: "How do you want to sign in?",
|
|
28175
|
+
options: [
|
|
28176
|
+
{ value: "browser", label: "In the browser", hint: "Google, GitHub or email" },
|
|
28177
|
+
{ value: "email", label: "With a code sent to my email", hint: "all in this terminal" }
|
|
28178
|
+
]
|
|
28179
|
+
})
|
|
28180
|
+
);
|
|
28181
|
+
if (how === "email") {
|
|
28182
|
+
const email = cancelled(await text({ message: "Email", validate: (v) => /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test((v ?? "").trim()) ? void 0 : "Enter your email address" }));
|
|
28183
|
+
return loginWithEmail({ ...opts, email }, api, dashboard);
|
|
28184
|
+
}
|
|
28185
|
+
}
|
|
27912
28186
|
const anon = createLiftbayClient({ baseUrl: api });
|
|
27913
28187
|
let pending = readPending(api);
|
|
27914
28188
|
if (!pending) {
|
|
@@ -27923,15 +28197,15 @@ async function login(opts) {
|
|
|
27923
28197
|
interval: start.interval
|
|
27924
28198
|
};
|
|
27925
28199
|
mkdirSync2(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
27926
|
-
|
|
28200
|
+
writeFileSync3(PENDING, JSON.stringify(pending), { mode: 384 });
|
|
27927
28201
|
if (opts.open !== false) openBrowser(pending.url);
|
|
27928
28202
|
}
|
|
27929
|
-
note(`${
|
|
28203
|
+
note(`${import_picocolors3.default.bold(pending.userCode)}
|
|
27930
28204
|
|
|
27931
|
-
${
|
|
28205
|
+
${import_picocolors3.default.dim("Confirm this code in your browser:")}
|
|
27932
28206
|
${pending.url}`, "Sign in");
|
|
27933
28207
|
if (opts.wait === false) {
|
|
27934
|
-
log.info(`Approve it in the browser, then run ${
|
|
28208
|
+
log.info(`Approve it in the browser, then run ${import_picocolors3.default.cyan(cmd("liftbay login"))} again to finish.`);
|
|
27935
28209
|
return { status: "pending", userCode: pending.userCode, url: pending.url, expiresAt: pending.expiresAt, next: "Approve in the browser, then run `liftbay login` to finish signing in" };
|
|
27936
28210
|
}
|
|
27937
28211
|
const spin = spinner();
|
|
@@ -27941,10 +28215,10 @@ ${pending.url}`, "Sign in");
|
|
|
27941
28215
|
const res = await unwrap(anon.POST("/v1/auth/device/token", { body: { deviceCode: pending.deviceCode } }));
|
|
27942
28216
|
clearPending();
|
|
27943
28217
|
saveCredentials({ apiUrl: api, dashboardUrl: pending.dashboardUrl, token: res.token, email: res.user.email });
|
|
27944
|
-
spin.stop(`Signed in as ${
|
|
28218
|
+
spin.stop(`Signed in as ${import_picocolors3.default.bold(res.user.email)}`);
|
|
27945
28219
|
return { status: "signed_in", user: res.user };
|
|
27946
28220
|
} catch (e) {
|
|
27947
|
-
if (e instanceof LiftbayApiError && e.code === "authorization_pending") {
|
|
28221
|
+
if (e instanceof LiftbayApiError && e.code === "authorization_pending" || isTransient(e)) {
|
|
27948
28222
|
await sleep(pending.interval * 1e3);
|
|
27949
28223
|
continue;
|
|
27950
28224
|
}
|
|
@@ -27975,8 +28249,8 @@ async function whoami() {
|
|
|
27975
28249
|
return { kind: "org_token", orgs: orgs.map((o) => ({ slug: o.slug, name: o.name })) };
|
|
27976
28250
|
}
|
|
27977
28251
|
const me = await unwrap(client().GET("/v1/auth/me"));
|
|
27978
|
-
console.log(`${
|
|
27979
|
-
for (const m of me.memberships) console.log(` ${m.orgSlug.padEnd(24)} ${
|
|
28252
|
+
console.log(`${import_picocolors3.default.bold(me.user.email)}${me.user.name ? ` (${me.user.name})` : ""}`);
|
|
28253
|
+
for (const m of me.memberships) console.log(` ${m.orgSlug.padEnd(24)} ${import_picocolors3.default.dim(m.role)}`);
|
|
27980
28254
|
return { kind: "user", user: me.user, memberships: me.memberships };
|
|
27981
28255
|
}
|
|
27982
28256
|
async function ensureSignedIn(opts) {
|
|
@@ -27984,13 +28258,15 @@ async function ensureSignedIn(opts) {
|
|
|
27984
28258
|
try {
|
|
27985
28259
|
if (process.env.LIFTBAY_TOKEN) return;
|
|
27986
28260
|
const me = await unwrap(client().GET("/v1/auth/me"));
|
|
27987
|
-
log.success(`Signed in as ${
|
|
28261
|
+
log.success(`Signed in as ${import_picocolors3.default.bold(me.user.email)}`);
|
|
27988
28262
|
return;
|
|
27989
28263
|
} catch (e) {
|
|
27990
28264
|
if (!(e instanceof LiftbayApiError && e.status === 401)) throw e;
|
|
27991
28265
|
}
|
|
27992
28266
|
}
|
|
27993
|
-
if (!isInteractive())
|
|
28267
|
+
if (!isInteractive()) {
|
|
28268
|
+
fail("Not signed in. Sign in with an email code: `liftbay login --email <address>`, then `liftbay login --email <address> --code <code>` (or approve in the browser: `liftbay login --no-wait`, then `liftbay login`; or set LIFTBAY_TOKEN). Then run this again.");
|
|
28269
|
+
}
|
|
27994
28270
|
await login(opts);
|
|
27995
28271
|
}
|
|
27996
28272
|
|
|
@@ -27998,33 +28274,217 @@ async function ensureSignedIn(opts) {
|
|
|
27998
28274
|
var import_picocolors7 = __toESM(require_picocolors(), 1);
|
|
27999
28275
|
var import_qrcode = __toESM(require_lib(), 1);
|
|
28000
28276
|
import { execFileSync } from "child_process";
|
|
28277
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
28278
|
+
import { dirname as dirname3 } from "path";
|
|
28001
28279
|
|
|
28002
|
-
// src/
|
|
28003
|
-
|
|
28004
|
-
|
|
28005
|
-
|
|
28280
|
+
// src/generated/workflow.ts
|
|
28281
|
+
var WORKFLOW_YAML = `# Liftbay builds on your own GitHub Actions minutes.
|
|
28282
|
+
# Liftbay starts this workflow; it does nothing on your own pushes or pull requests. With this file on the
|
|
28283
|
+
# default branch, Liftbay uses workflow_dispatch. Otherwise (a branch you're trying out), Liftbay points a
|
|
28284
|
+
# temporary liftbay/build/<platform>/<build id> branch at your commit, which runs this file from that commit,
|
|
28285
|
+
# and deletes it once the build has started.
|
|
28286
|
+
# The build gets no repository secrets: the runner exchanges this run's GitHub OIDC token for a
|
|
28287
|
+
# one-time Liftbay job token, checked against this repository and this workflow file.
|
|
28288
|
+
# Docs: https://github.com/liftbay/runner
|
|
28289
|
+
name: Liftbay build
|
|
28290
|
+
run-name: Liftbay build \${{ inputs.build_id || github.ref_name }}
|
|
28006
28291
|
|
|
28007
|
-
|
|
28008
|
-
|
|
28009
|
-
|
|
28010
|
-
|
|
28011
|
-
|
|
28292
|
+
on:
|
|
28293
|
+
workflow_dispatch:
|
|
28294
|
+
inputs:
|
|
28295
|
+
build_id:
|
|
28296
|
+
description: Liftbay build id
|
|
28297
|
+
required: true
|
|
28298
|
+
type: string
|
|
28299
|
+
platform:
|
|
28300
|
+
description: Platform
|
|
28301
|
+
required: true
|
|
28302
|
+
type: choice
|
|
28303
|
+
options: [android, ios]
|
|
28304
|
+
api_url:
|
|
28305
|
+
description: Liftbay API
|
|
28306
|
+
required: false
|
|
28307
|
+
type: string
|
|
28308
|
+
default: https://api.liftbay.dev
|
|
28309
|
+
push:
|
|
28310
|
+
branches: ["liftbay/build/**"]
|
|
28012
28311
|
|
|
28013
|
-
|
|
28014
|
-
|
|
28015
|
-
|
|
28016
|
-
|
|
28017
|
-
|
|
28018
|
-
|
|
28019
|
-
|
|
28020
|
-
|
|
28021
|
-
|
|
28022
|
-
|
|
28023
|
-
|
|
28024
|
-
|
|
28025
|
-
|
|
28026
|
-
|
|
28027
|
-
|
|
28312
|
+
permissions:
|
|
28313
|
+
contents: read
|
|
28314
|
+
id-token: write
|
|
28315
|
+
|
|
28316
|
+
env:
|
|
28317
|
+
# Pinned runner release. The archive's SHA-256 is checked before the runner starts.
|
|
28318
|
+
# Release notes and provenance: https://github.com/liftbay/runner/releases/tag/v0.1.2
|
|
28319
|
+
RUNNER_VERSION: "0.1.2"
|
|
28320
|
+
RUNNER_SHA256_LINUX_AMD64: "9b37bfe534b6dc40f81182bdb9dc46461a672f2c28f048743dc8f13f630909b1"
|
|
28321
|
+
RUNNER_SHA256_DARWIN_ARM64: "301bf09358a47b48df0cdbd6f60edbac1227e4d7f61e037959d4009ed6ae7f27"
|
|
28322
|
+
|
|
28323
|
+
jobs:
|
|
28324
|
+
android:
|
|
28325
|
+
if: inputs.platform == 'android' || startsWith(github.ref_name, 'liftbay/build/android/')
|
|
28326
|
+
runs-on: ubuntu-latest
|
|
28327
|
+
timeout-minutes: 60
|
|
28328
|
+
steps:
|
|
28329
|
+
# GitHub's Linux runners start with about 14 GB free; a React Native release build (NDKs, CMake, four
|
|
28330
|
+
# architectures, Gradle caches) can use more. Remove preinstalled toolchains no app build uses.
|
|
28331
|
+
- name: Free disk space
|
|
28332
|
+
run: |
|
|
28333
|
+
df -h / | tail -1
|
|
28334
|
+
sudo rm -rf /usr/share/dotnet /usr/local/.ghcup /opt/ghc /opt/hostedtoolcache/CodeQL /usr/local/share/boost /usr/share/swift /usr/local/lib/node_modules /usr/local/share/powershell /usr/local/share/chromium /opt/microsoft /opt/google/chrome || true
|
|
28335
|
+
sudo docker image prune --all --force >/dev/null 2>&1 || true
|
|
28336
|
+
df -h / | tail -1
|
|
28337
|
+
|
|
28338
|
+
- uses: actions/checkout@v4
|
|
28339
|
+
|
|
28340
|
+
- uses: actions/setup-java@v4
|
|
28341
|
+
with:
|
|
28342
|
+
distribution: temurin
|
|
28343
|
+
java-version: "17"
|
|
28344
|
+
|
|
28345
|
+
- uses: actions/setup-node@v4
|
|
28346
|
+
with:
|
|
28347
|
+
node-version: "22"
|
|
28348
|
+
|
|
28349
|
+
- name: Cache JavaScript dependencies
|
|
28350
|
+
uses: actions/cache@v4
|
|
28351
|
+
with:
|
|
28352
|
+
path: |
|
|
28353
|
+
node_modules
|
|
28354
|
+
~/.npm
|
|
28355
|
+
~/.cache/yarn
|
|
28356
|
+
~/.local/share/pnpm/store
|
|
28357
|
+
~/.bun/install/cache
|
|
28358
|
+
key: js-\${{ runner.os }}-\${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb') }}
|
|
28359
|
+
restore-keys: js-\${{ runner.os }}-
|
|
28360
|
+
|
|
28361
|
+
- name: Cache Gradle
|
|
28362
|
+
uses: actions/cache@v4
|
|
28363
|
+
with:
|
|
28364
|
+
path: |
|
|
28365
|
+
~/.gradle/caches
|
|
28366
|
+
~/.gradle/wrapper
|
|
28367
|
+
key: gradle-\${{ runner.os }}-\${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb', 'android/**/*.gradle*', 'android/gradle/wrapper/gradle-wrapper.properties') }}
|
|
28368
|
+
restore-keys: gradle-\${{ runner.os }}-
|
|
28369
|
+
|
|
28370
|
+
- name: Install the Liftbay runner
|
|
28371
|
+
env:
|
|
28372
|
+
# Bring your own runner: set the repository variables LIFTBAY_RUNNER_URL (a
|
|
28373
|
+
# liftbay-runner tar.gz you built) and LIFTBAY_RUNNER_SHA256.
|
|
28374
|
+
RUNNER_URL: \${{ vars.LIFTBAY_RUNNER_URL }}
|
|
28375
|
+
RUNNER_SHA256: \${{ vars.LIFTBAY_RUNNER_SHA256 || env.RUNNER_SHA256_LINUX_AMD64 }}
|
|
28376
|
+
run: |
|
|
28377
|
+
set -euo pipefail
|
|
28378
|
+
url="\${RUNNER_URL:-https://github.com/liftbay/runner/releases/download/v\${RUNNER_VERSION}/liftbay-runner_\${RUNNER_VERSION}_linux_amd64.tar.gz}"
|
|
28379
|
+
curl -fsSL --retry 3 -o "$RUNNER_TEMP/liftbay-runner.tar.gz" "$url"
|
|
28380
|
+
echo "\${RUNNER_SHA256} $RUNNER_TEMP/liftbay-runner.tar.gz" | sha256sum -c -
|
|
28381
|
+
tar -xzf "$RUNNER_TEMP/liftbay-runner.tar.gz" -C "$RUNNER_TEMP" liftbay-runner
|
|
28382
|
+
"$RUNNER_TEMP/liftbay-runner" version
|
|
28383
|
+
|
|
28384
|
+
- name: Build
|
|
28385
|
+
env:
|
|
28386
|
+
LIFTBAY_BUILD_ID: \${{ inputs.build_id }}
|
|
28387
|
+
LIFTBAY_API_URL: \${{ inputs.api_url || vars.LIFTBAY_API_URL || 'https://api.liftbay.dev' }}
|
|
28388
|
+
# On a liftbay/build/<platform>/<build id> branch, the build id is the branch name's last part.
|
|
28389
|
+
run: 'LIFTBAY_BUILD_ID="\${LIFTBAY_BUILD_ID:-\${GITHUB_REF_NAME##*/}}" "$RUNNER_TEMP/liftbay-runner" run'
|
|
28390
|
+
|
|
28391
|
+
ios:
|
|
28392
|
+
if: inputs.platform == 'ios' || startsWith(github.ref_name, 'liftbay/build/ios/')
|
|
28393
|
+
runs-on: macos-15
|
|
28394
|
+
timeout-minutes: 90
|
|
28395
|
+
steps:
|
|
28396
|
+
- uses: actions/checkout@v4
|
|
28397
|
+
|
|
28398
|
+
- uses: maxim-lobanov/setup-xcode@v1
|
|
28399
|
+
with:
|
|
28400
|
+
xcode-version: latest-stable
|
|
28401
|
+
|
|
28402
|
+
- uses: actions/setup-node@v4
|
|
28403
|
+
with:
|
|
28404
|
+
node-version: "22"
|
|
28405
|
+
|
|
28406
|
+
- name: Cache JavaScript dependencies
|
|
28407
|
+
uses: actions/cache@v4
|
|
28408
|
+
with:
|
|
28409
|
+
path: |
|
|
28410
|
+
node_modules
|
|
28411
|
+
~/.npm
|
|
28412
|
+
~/Library/Caches/Yarn
|
|
28413
|
+
~/Library/pnpm/store
|
|
28414
|
+
~/.bun/install/cache
|
|
28415
|
+
key: js-\${{ runner.os }}-\${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb') }}
|
|
28416
|
+
restore-keys: js-\${{ runner.os }}-
|
|
28417
|
+
|
|
28418
|
+
# Only the download caches: a restored ios/Pods would make expo prebuild see a half project.
|
|
28419
|
+
- name: Cache CocoaPods
|
|
28420
|
+
uses: actions/cache@v4
|
|
28421
|
+
with:
|
|
28422
|
+
path: |
|
|
28423
|
+
~/Library/Caches/CocoaPods
|
|
28424
|
+
~/.cocoapods
|
|
28425
|
+
key: pods-\${{ runner.os }}-\${{ hashFiles('ios/Podfile.lock', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb') }}
|
|
28426
|
+
restore-keys: pods-\${{ runner.os }}-
|
|
28427
|
+
|
|
28428
|
+
- name: Install the Liftbay runner
|
|
28429
|
+
env:
|
|
28430
|
+
RUNNER_URL: \${{ vars.LIFTBAY_RUNNER_URL_MACOS }}
|
|
28431
|
+
RUNNER_SHA256: \${{ vars.LIFTBAY_RUNNER_SHA256_MACOS || env.RUNNER_SHA256_DARWIN_ARM64 }}
|
|
28432
|
+
run: |
|
|
28433
|
+
set -euo pipefail
|
|
28434
|
+
url="\${RUNNER_URL:-https://github.com/liftbay/runner/releases/download/v\${RUNNER_VERSION}/liftbay-runner_\${RUNNER_VERSION}_darwin_arm64.tar.gz}"
|
|
28435
|
+
curl -fsSL --retry 3 -o "$RUNNER_TEMP/liftbay-runner.tar.gz" "$url"
|
|
28436
|
+
echo "\${RUNNER_SHA256} $RUNNER_TEMP/liftbay-runner.tar.gz" | shasum -a 256 -c -
|
|
28437
|
+
tar -xzf "$RUNNER_TEMP/liftbay-runner.tar.gz" -C "$RUNNER_TEMP" liftbay-runner
|
|
28438
|
+
"$RUNNER_TEMP/liftbay-runner" version
|
|
28439
|
+
|
|
28440
|
+
- name: Build
|
|
28441
|
+
env:
|
|
28442
|
+
LIFTBAY_BUILD_ID: \${{ inputs.build_id }}
|
|
28443
|
+
LIFTBAY_API_URL: \${{ inputs.api_url || vars.LIFTBAY_API_URL || 'https://api.liftbay.dev' }}
|
|
28444
|
+
# On a liftbay/build/<platform>/<build id> branch, the build id is the branch name's last part.
|
|
28445
|
+
run: 'LIFTBAY_BUILD_ID="\${LIFTBAY_BUILD_ID:-\${GITHUB_REF_NAME##*/}}" "$RUNNER_TEMP/liftbay-runner" run'
|
|
28446
|
+
`;
|
|
28447
|
+
|
|
28448
|
+
// src/workflow.ts
|
|
28449
|
+
var WORKFLOW_PATH = ".github/workflows/liftbay.yml";
|
|
28450
|
+
async function currentWorkflow() {
|
|
28451
|
+
try {
|
|
28452
|
+
const res = await fetch(`${apiUrl()}/v1/github/workflow`, { signal: AbortSignal.timeout(5e3) });
|
|
28453
|
+
if (res.ok) {
|
|
28454
|
+
const body = await res.json();
|
|
28455
|
+
if (typeof body.content === "string" && body.content.includes("liftbay")) return body.content;
|
|
28456
|
+
}
|
|
28457
|
+
} catch {
|
|
28458
|
+
}
|
|
28459
|
+
return WORKFLOW_YAML;
|
|
28460
|
+
}
|
|
28461
|
+
|
|
28462
|
+
// src/commands/credentials.ts
|
|
28463
|
+
import { chmodSync as chmodSync3, existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
28464
|
+
import { basename, dirname as dirname2, extname, join as join6, resolve } from "path";
|
|
28465
|
+
var import_picocolors5 = __toESM(require_picocolors(), 1);
|
|
28466
|
+
|
|
28467
|
+
// src/apple/session.ts
|
|
28468
|
+
import { createHash as createHash3 } from "crypto";
|
|
28469
|
+
import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
28470
|
+
import { homedir as homedir2 } from "os";
|
|
28471
|
+
import { join as join5 } from "path";
|
|
28472
|
+
|
|
28473
|
+
// src/apple/cookies.ts
|
|
28474
|
+
var CookieJar = class {
|
|
28475
|
+
cookies = [];
|
|
28476
|
+
constructor(saved = []) {
|
|
28477
|
+
this.cookies = saved.filter((c2) => !isExpired(c2));
|
|
28478
|
+
}
|
|
28479
|
+
/** Stores every Set-Cookie of a response from `url`. */
|
|
28480
|
+
store(url, setCookies, now = Date.now()) {
|
|
28481
|
+
const { hostname, pathname } = new URL(url);
|
|
28482
|
+
for (const header of setCookies) {
|
|
28483
|
+
const c2 = parse(header, hostname, pathname, now);
|
|
28484
|
+
if (!c2 || !domainMatches(hostname, c2)) continue;
|
|
28485
|
+
this.cookies = this.cookies.filter((o) => !(o.name === c2.name && o.domain === c2.domain && o.path === c2.path));
|
|
28486
|
+
if (!isExpired(c2, now)) this.cookies.push(c2);
|
|
28487
|
+
}
|
|
28028
28488
|
}
|
|
28029
28489
|
/** The Cookie header for a request to `url`, or "" when none apply. */
|
|
28030
28490
|
header(url, now = Date.now()) {
|
|
@@ -28186,9 +28646,9 @@ var IDMSA = "https://idmsa.apple.com/appleauth";
|
|
|
28186
28646
|
var ASC = "https://appstoreconnect.apple.com";
|
|
28187
28647
|
var PORTALS = ["https://developer.apple.com", "https://developer-mdn.apple.com"];
|
|
28188
28648
|
var MAX_CODE_ATTEMPTS = 3;
|
|
28189
|
-
var sessionDir = (configDir) => configDir ??
|
|
28649
|
+
var sessionDir = (configDir) => configDir ?? join5(process.env.LIFTBAY_CONFIG_DIR ?? join5(homedir2(), ".liftbay"), "apple");
|
|
28190
28650
|
var accountName = (appleId) => appleId.trim().toLowerCase();
|
|
28191
|
-
var sessionFile = (appleId, configDir) =>
|
|
28651
|
+
var sessionFile = (appleId, configDir) => join5(sessionDir(configDir), `${createHash3("sha256").update(accountName(appleId)).digest("hex")}.json`);
|
|
28192
28652
|
var b64 = (b) => Buffer.from(b).toString("base64");
|
|
28193
28653
|
function serviceError(data) {
|
|
28194
28654
|
if (!data || typeof data !== "object") return null;
|
|
@@ -28211,9 +28671,9 @@ var AppleSession = class _AppleSession {
|
|
|
28211
28671
|
/** Signs in (SRP + hashcash + 2FA), reusing a saved session from ~/.liftbay/apple/<sha256(appleId)>.json (0600) when still valid. */
|
|
28212
28672
|
static async signIn(appleId, prompts, opts = {}) {
|
|
28213
28673
|
const file2 = sessionFile(appleId, opts.configDir);
|
|
28214
|
-
if (
|
|
28674
|
+
if (existsSync4(file2)) {
|
|
28215
28675
|
try {
|
|
28216
|
-
const saved = JSON.parse(
|
|
28676
|
+
const saved = JSON.parse(readFileSync4(file2, "utf8"));
|
|
28217
28677
|
const restored = new _AppleSession(appleId, new CookieJar(saved.cookies), opts);
|
|
28218
28678
|
if (await restored.fetchOlympus()) {
|
|
28219
28679
|
opts.log?.(`Using the saved Apple session for ${appleId}`);
|
|
@@ -28242,7 +28702,7 @@ var AppleSession = class _AppleSession {
|
|
|
28242
28702
|
const dir = sessionDir(this.opts.configDir);
|
|
28243
28703
|
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
28244
28704
|
const file2 = sessionFile(this.appleId, this.opts.configDir);
|
|
28245
|
-
|
|
28705
|
+
writeFileSync4(file2, JSON.stringify({ appleId: accountName(this.appleId), cookies: this.jar.toJSON() }), { mode: 384 });
|
|
28246
28706
|
chmodSync2(file2, 384);
|
|
28247
28707
|
}
|
|
28248
28708
|
async login(password2, prompts) {
|
|
@@ -28474,120 +28934,14 @@ function apiKeyError(r2) {
|
|
|
28474
28934
|
return new AppleAuthError("unexpected", `App Store Connect refused to create an API key (${r2.status})${text2.trim() ? `: ${text2.trim()}` : ""}`);
|
|
28475
28935
|
}
|
|
28476
28936
|
|
|
28477
|
-
// src/project.ts
|
|
28478
|
-
import { execFile, spawn as spawn2 } from "child_process";
|
|
28479
|
-
import { promisify } from "util";
|
|
28480
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
28481
|
-
import { join as join5 } from "path";
|
|
28482
|
-
var execFileAsync = promisify(execFile);
|
|
28483
|
-
var CONFIG_FILE = "liftbay.json";
|
|
28484
|
-
function readConfig(cwd = process.cwd()) {
|
|
28485
|
-
const path2 = join5(cwd, CONFIG_FILE);
|
|
28486
|
-
return existsSync4(path2) ? JSON.parse(readFileSync4(path2, "utf8")) : null;
|
|
28487
|
-
}
|
|
28488
|
-
function requireConfig(cwd = process.cwd()) {
|
|
28489
|
-
return readConfig(cwd) ?? fail(`No ${CONFIG_FILE} here. Run \`liftbay init\` in your app's folder.`);
|
|
28490
|
-
}
|
|
28491
|
-
function writeConfig(config, cwd = process.cwd()) {
|
|
28492
|
-
writeFileSync4(join5(cwd, CONFIG_FILE), `${JSON.stringify({ $schema: "https://liftbay.dev/schema/liftbay.json", ...config }, null, 2)}
|
|
28493
|
-
`);
|
|
28494
|
-
}
|
|
28495
|
-
function readJson(path2) {
|
|
28496
|
-
return existsSync4(path2) ? JSON.parse(readFileSync4(path2, "utf8")) : null;
|
|
28497
|
-
}
|
|
28498
|
-
function packageManager(cwd = process.cwd()) {
|
|
28499
|
-
if (existsSync4(join5(cwd, "bun.lock")) || existsSync4(join5(cwd, "bun.lockb"))) return "bun";
|
|
28500
|
-
if (existsSync4(join5(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
28501
|
-
if (existsSync4(join5(cwd, "yarn.lock"))) return "yarn";
|
|
28502
|
-
return "npm";
|
|
28503
|
-
}
|
|
28504
|
-
async function output(cmd2, args, cwd = process.cwd()) {
|
|
28505
|
-
const { stdout: stdout2 } = await execFileAsync(cmd2, args, { cwd, encoding: "utf8", maxBuffer: 50 * 1024 * 1024 });
|
|
28506
|
-
return stdout2;
|
|
28507
|
-
}
|
|
28508
|
-
async function expoConfig(cwd = process.cwd()) {
|
|
28509
|
-
return JSON.parse(await output("npx", ["expo", "config", "--json", "--type", "public"], cwd));
|
|
28510
|
-
}
|
|
28511
|
-
async function detectApp(cwd = process.cwd()) {
|
|
28512
|
-
const pkg = readJson(join5(cwd, "package.json"));
|
|
28513
|
-
if (!pkg) fail("No package.json here. Run this in your React Native app's folder.");
|
|
28514
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
28515
|
-
if (!deps["react-native"]) fail("This doesn't look like a React Native app (react-native is not a dependency).");
|
|
28516
|
-
const hasExpo = !!deps.expo;
|
|
28517
|
-
let name = pkg.name ?? "app";
|
|
28518
|
-
let slug = name;
|
|
28519
|
-
let version = pkg.version ?? null;
|
|
28520
|
-
let bundleId = null;
|
|
28521
|
-
let androidPackage = null;
|
|
28522
|
-
if (hasExpo) {
|
|
28523
|
-
const cfg = await expoConfig(cwd);
|
|
28524
|
-
name = cfg.name ?? name;
|
|
28525
|
-
slug = cfg.slug ?? slug;
|
|
28526
|
-
version = cfg.version ?? version;
|
|
28527
|
-
bundleId = cfg.ios?.bundleIdentifier ?? null;
|
|
28528
|
-
androidPackage = cfg.android?.package ?? null;
|
|
28529
|
-
} else {
|
|
28530
|
-
const gradle = join5(cwd, "android/app/build.gradle");
|
|
28531
|
-
if (existsSync4(gradle)) androidPackage = readFileSync4(gradle, "utf8").match(/applicationId\s+["']([^"']+)["']/)?.[1] ?? null;
|
|
28532
|
-
}
|
|
28533
|
-
return {
|
|
28534
|
-
framework: hasExpo ? "expo" : "react-native",
|
|
28535
|
-
name,
|
|
28536
|
-
slug: String(slug).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "app",
|
|
28537
|
-
version,
|
|
28538
|
-
bundleId,
|
|
28539
|
-
androidPackage,
|
|
28540
|
-
hasExpoModules: hasExpo,
|
|
28541
|
-
hasExpoUpdates: !!deps["expo-updates"],
|
|
28542
|
-
packageManager: packageManager(cwd),
|
|
28543
|
-
appJson: existsSync4(join5(cwd, "app.json"))
|
|
28544
|
-
};
|
|
28545
|
-
}
|
|
28546
|
-
async function run(cmd2, args, cwd = process.cwd(), env2 = {}) {
|
|
28547
|
-
const code = await new Promise((resolve4, reject) => {
|
|
28548
|
-
const child = spawn2(cmd2, args, { cwd, stdio: "inherit", env: { ...process.env, ...env2 } });
|
|
28549
|
-
child.on("error", reject);
|
|
28550
|
-
child.on("close", resolve4);
|
|
28551
|
-
});
|
|
28552
|
-
if (code !== 0) fail(`\`${cmd2} ${args.join(" ")}\` failed (exit ${code})`);
|
|
28553
|
-
}
|
|
28554
|
-
async function runQuiet(label, cmd2, args, cwd = process.cwd()) {
|
|
28555
|
-
const spin = spinner();
|
|
28556
|
-
spin.start(label);
|
|
28557
|
-
const lines = [];
|
|
28558
|
-
const code = await new Promise((resolve4, reject) => {
|
|
28559
|
-
const child = spawn2(cmd2, args, {
|
|
28560
|
-
cwd,
|
|
28561
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
28562
|
-
env: { ...process.env, npm_config_audit: "false", npm_config_fund: "false", npm_config_update_notifier: "false" }
|
|
28563
|
-
});
|
|
28564
|
-
const keep = (chunk) => lines.push(...chunk.toString().split(/\r?\n/).filter(Boolean));
|
|
28565
|
-
child.stdout.on("data", keep);
|
|
28566
|
-
child.stderr.on("data", keep);
|
|
28567
|
-
child.on("error", reject);
|
|
28568
|
-
child.on("close", resolve4);
|
|
28569
|
-
});
|
|
28570
|
-
if (code === 0) return spin.stop(label.replace(/^Installing/, "Installed").replace(/^Setting up/, "Set up"));
|
|
28571
|
-
spin.error(`${label} failed`);
|
|
28572
|
-
console.error(lines.slice(-40).join("\n"));
|
|
28573
|
-
fail(`\`${cmd2} ${args.join(" ")}\` failed (exit ${code})`);
|
|
28574
|
-
}
|
|
28575
|
-
function readAppJson(cwd = process.cwd()) {
|
|
28576
|
-
return readJson(join5(cwd, "app.json"));
|
|
28577
|
-
}
|
|
28578
|
-
function writeAppJson(data, cwd = process.cwd()) {
|
|
28579
|
-
writeFileSync4(join5(cwd, "app.json"), `${JSON.stringify(data, null, 2)}
|
|
28580
|
-
`);
|
|
28581
|
-
}
|
|
28582
|
-
|
|
28583
28937
|
// src/prompts.ts
|
|
28584
|
-
var
|
|
28938
|
+
var import_picocolors4 = __toESM(require_picocolors(), 1);
|
|
28585
28939
|
function fit(text2, reserve = 8) {
|
|
28586
28940
|
const width = Math.max(20, (process.stdout.columns || 80) - reserve);
|
|
28587
28941
|
const plain = text2.replace(/\x1b\[[0-9;]*m/g, "");
|
|
28588
28942
|
return plain.length <= width ? text2 : `${plain.slice(0, width - 1)}\u2026`;
|
|
28589
28943
|
}
|
|
28590
|
-
var symbol2 = (state) => state === "submit" ?
|
|
28944
|
+
var symbol2 = (state) => state === "submit" ? import_picocolors4.default.green("\u25C7") : state === "cancel" ? import_picocolors4.default.red("\u25A0") : state === "error" ? import_picocolors4.default.yellow("\u25B2") : import_picocolors4.default.cyan("\u25C6");
|
|
28591
28945
|
function checkboxes(opts) {
|
|
28592
28946
|
const required = opts.required ?? true;
|
|
28593
28947
|
return new a({
|
|
@@ -28599,121 +28953,28 @@ function checkboxes(opts) {
|
|
|
28599
28953
|
},
|
|
28600
28954
|
render() {
|
|
28601
28955
|
const selected = this.value ?? [];
|
|
28602
|
-
const bar = this.state === "error" ?
|
|
28603
|
-
const title = `${
|
|
28956
|
+
const bar = this.state === "error" ? import_picocolors4.default.yellow("\u2502") : this.state === "submit" || this.state === "cancel" ? import_picocolors4.default.gray("\u2502") : import_picocolors4.default.cyan("\u2502");
|
|
28957
|
+
const title = `${import_picocolors4.default.gray("\u2502")}
|
|
28604
28958
|
${symbol2(this.state)} ${opts.message}
|
|
28605
28959
|
`;
|
|
28606
28960
|
if (this.state === "submit" || this.state === "cancel") {
|
|
28607
28961
|
const chosen = this.options.filter((o) => selected.includes(o.value)).map((o) => o.label);
|
|
28608
|
-
const text2 = chosen.join(", ") || "none";
|
|
28609
|
-
return `${title}${bar} ${this.state === "cancel" ?
|
|
28610
|
-
}
|
|
28611
|
-
const lines = this.options.map((o, i2) => {
|
|
28612
|
-
const on = selected.includes(o.value);
|
|
28613
|
-
const active = i2 === this.cursor;
|
|
28614
|
-
const box = on ?
|
|
28615
|
-
const label = active ?
|
|
28616
|
-
return `${bar} ${box} ${label}${o.hint ? ` ${
|
|
28617
|
-
});
|
|
28618
|
-
const footer = this.state === "error" ? `${
|
|
28619
|
-
return `${title}${lines.join("\n")}
|
|
28620
|
-
${footer}
|
|
28621
|
-
`;
|
|
28622
|
-
}
|
|
28623
|
-
}).prompt();
|
|
28624
|
-
}
|
|
28625
|
-
|
|
28626
|
-
// src/commands/link.ts
|
|
28627
|
-
var import_picocolors4 = __toESM(require_picocolors(), 1);
|
|
28628
|
-
function cancelled(value) {
|
|
28629
|
-
if (isCancel(value)) {
|
|
28630
|
-
cancel("Cancelled");
|
|
28631
|
-
process.exit(0);
|
|
28632
|
-
}
|
|
28633
|
-
return value;
|
|
28634
|
-
}
|
|
28635
|
-
var slugifyProject = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
|
|
28636
|
-
var slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
28637
|
-
async function createFirstOrg(lb, given) {
|
|
28638
|
-
const me = await unwrap(lb.GET("/v1/auth/me"));
|
|
28639
|
-
const suggested = given ?? (me.user.name?.trim() || me.user.email.split("@")[0]);
|
|
28640
|
-
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;
|
|
28641
|
-
const base = slugify(name).padEnd(2, "0");
|
|
28642
|
-
for (const slug of [base, `${base.slice(0, 35)}-${Math.random().toString(36).slice(2, 6)}`]) {
|
|
28643
|
-
const res = await lb.POST("/v1/orgs", { body: { name: name.slice(0, 64), slug } });
|
|
28644
|
-
if (res.data) {
|
|
28645
|
-
log.success(`Created organization ${import_picocolors4.default.bold(res.data.name)} ${import_picocolors4.default.dim(`(liftbay.dev/${res.data.slug})`)}`);
|
|
28646
|
-
return res.data;
|
|
28647
|
-
}
|
|
28648
|
-
if (res.response.status !== 409) await unwrap(Promise.resolve(res));
|
|
28649
|
-
}
|
|
28650
|
-
return fail("Couldn't pick a free organization URL. Pass --org <slug>.");
|
|
28651
|
-
}
|
|
28652
|
-
async function ensureLinked(app, opts) {
|
|
28653
|
-
const existing = readConfig();
|
|
28654
|
-
if (existing) {
|
|
28655
|
-
log.success(`Linked to ${import_picocolors4.default.bold(`${existing.org}/${existing.project}`)}`);
|
|
28656
|
-
return existing;
|
|
28657
|
-
}
|
|
28658
|
-
const lb = client();
|
|
28659
|
-
const orgs = await unwrap(lb.GET("/v1/orgs"));
|
|
28660
|
-
if (!orgs.length) orgs.push(await createFirstOrg(lb, opts.org));
|
|
28661
|
-
let org = opts.org ?? (orgs.length === 1 ? orgs[0].slug : void 0);
|
|
28662
|
-
if (!org) {
|
|
28663
|
-
if (!isInteractive()) fail(`Several organizations available; pass --org (${orgs.map((o) => o.slug).join(", ")})`);
|
|
28664
|
-
org = cancelled(await select({ message: "Organization", options: orgs.map((o) => ({ value: o.slug, label: o.name, hint: o.slug })) }));
|
|
28665
|
-
}
|
|
28666
|
-
const projects = await unwrap(lb.GET("/v1/orgs/{org}/projects", { params: { path: { org } } }));
|
|
28667
|
-
let slug = opts.project;
|
|
28668
|
-
let projectName;
|
|
28669
|
-
if (!slug) {
|
|
28670
|
-
const match = projects.find((pr) => pr.slug === app.slug);
|
|
28671
|
-
if (opts.yes || !isInteractive()) slug = match?.slug ?? app.slug;
|
|
28672
|
-
else {
|
|
28673
|
-
const taken = projects.some((pr) => pr.slug === app.slug);
|
|
28674
|
-
const choice = cancelled(
|
|
28675
|
-
await select({
|
|
28676
|
-
message: "Liftbay project",
|
|
28677
|
-
options: [
|
|
28678
|
-
...taken ? [] : [{ value: "__new__", label: `Create "${app.name}"`, hint: `new project \xB7 liftbay.dev/${org}/${app.slug}` }],
|
|
28679
|
-
{ value: "__named__", label: "Create a project with another name" },
|
|
28680
|
-
...projects.map((pr) => ({ value: pr.slug, label: pr.name, hint: `existing \xB7 ${pr.slug}` }))
|
|
28681
|
-
],
|
|
28682
|
-
initialValue: match?.slug ?? (taken ? "__named__" : "__new__")
|
|
28683
|
-
})
|
|
28684
|
-
);
|
|
28685
|
-
if (choice === "__named__") {
|
|
28686
|
-
projectName = cancelled(
|
|
28687
|
-
await text({
|
|
28688
|
-
message: "Project name",
|
|
28689
|
-
initialValue: app.name,
|
|
28690
|
-
validate: (v) => {
|
|
28691
|
-
const s = slugifyProject(v ?? "");
|
|
28692
|
-
if (s.length < 2) return "At least 2 letters or digits";
|
|
28693
|
-
if (projects.some((pr) => pr.slug === s)) return `${s} already exists: pick it from the list instead`;
|
|
28694
|
-
}
|
|
28695
|
-
})
|
|
28696
|
-
);
|
|
28697
|
-
slug = slugifyProject(projectName);
|
|
28698
|
-
} else slug = choice === "__new__" ? app.slug : choice;
|
|
28699
|
-
}
|
|
28700
|
-
}
|
|
28701
|
-
let project2 = projects.find((pr) => pr.slug === slug);
|
|
28702
|
-
if (!project2) {
|
|
28703
|
-
project2 = await unwrap(
|
|
28704
|
-
lb.POST("/v1/orgs/{org}/projects", {
|
|
28705
|
-
params: { path: { org } },
|
|
28706
|
-
body: { name: projectName ?? app.name, slug, framework: app.framework, bundleId: app.bundleId, androidPackage: app.androidPackage }
|
|
28707
|
-
})
|
|
28708
|
-
);
|
|
28709
|
-
log.success(`Created project ${import_picocolors4.default.bold(`${org}/${project2.slug}`)} with development, preview and production environments`);
|
|
28710
|
-
} else {
|
|
28711
|
-
log.success(`Linked to existing project ${import_picocolors4.default.bold(`${org}/${project2.slug}`)}`);
|
|
28712
|
-
}
|
|
28713
|
-
const config = { org, project: project2.slug, projectId: project2.id };
|
|
28714
|
-
writeConfig(config);
|
|
28715
|
-
log.info(`Wrote ${import_picocolors4.default.cyan(cmd("liftbay.json"))} ${import_picocolors4.default.dim("(commit it: it links this repository to the project)")}`);
|
|
28716
|
-
return config;
|
|
28962
|
+
const text2 = chosen.join(", ") || "none";
|
|
28963
|
+
return `${title}${bar} ${this.state === "cancel" ? import_picocolors4.default.strikethrough(import_picocolors4.default.dim(text2)) : import_picocolors4.default.dim(text2)}`;
|
|
28964
|
+
}
|
|
28965
|
+
const lines = this.options.map((o, i2) => {
|
|
28966
|
+
const on = selected.includes(o.value);
|
|
28967
|
+
const active = i2 === this.cursor;
|
|
28968
|
+
const box = on ? import_picocolors4.default.green("[\u2714]") : active ? import_picocolors4.default.cyan("[ ]") : import_picocolors4.default.dim("[ ]");
|
|
28969
|
+
const label = active ? import_picocolors4.default.bold(o.label) : o.label;
|
|
28970
|
+
return `${bar} ${box} ${label}${o.hint ? ` ${import_picocolors4.default.dim(`(${o.hint})`)}` : ""}`;
|
|
28971
|
+
});
|
|
28972
|
+
const footer = this.state === "error" ? `${import_picocolors4.default.yellow("\u2514")} ${import_picocolors4.default.yellow(this.error)}` : `${import_picocolors4.default.cyan("\u2514")} ${import_picocolors4.default.dim("\u2191/\u2193 move \xB7 space select \xB7 enter confirm")}`;
|
|
28973
|
+
return `${title}${lines.join("\n")}
|
|
28974
|
+
${footer}
|
|
28975
|
+
`;
|
|
28976
|
+
}
|
|
28977
|
+
}).prompt();
|
|
28717
28978
|
}
|
|
28718
28979
|
|
|
28719
28980
|
// src/commands/credentials.ts
|
|
@@ -28992,7 +29253,7 @@ async function waitFor(lb, config, message, done) {
|
|
|
28992
29253
|
const until = Date.now() + WAIT_MINUTES * 6e4;
|
|
28993
29254
|
while (Date.now() < until) {
|
|
28994
29255
|
await sleep2(4e3);
|
|
28995
|
-
const s = await status(lb, config);
|
|
29256
|
+
const s = await poll(() => status(lb, config));
|
|
28996
29257
|
if (done(s)) {
|
|
28997
29258
|
spin.stop(message.replace(/^Waiting for /, "Done: "));
|
|
28998
29259
|
return s;
|
|
@@ -29005,7 +29266,7 @@ async function waitForInstall(lb, config, attempt) {
|
|
|
29005
29266
|
const spin = spinner();
|
|
29006
29267
|
spin.start(fit("Waiting for the GitHub App to be installed"));
|
|
29007
29268
|
const explain = (text2, spinnerText) => {
|
|
29008
|
-
spin.
|
|
29269
|
+
spin.clear();
|
|
29009
29270
|
log.info(text2);
|
|
29010
29271
|
spin.start(fit(spinnerText));
|
|
29011
29272
|
};
|
|
@@ -29014,7 +29275,7 @@ async function waitForInstall(lb, config, attempt) {
|
|
|
29014
29275
|
let last = "waiting";
|
|
29015
29276
|
while (Date.now() < until) {
|
|
29016
29277
|
await sleep2(3e3);
|
|
29017
|
-
const a2 = attempt ? (await lb.GET("/v1/orgs/{org}/github/install/{attempt}", { params: { path: { org: config.org, attempt } } })).data : void 0;
|
|
29278
|
+
const a2 = attempt ? (await poll(() => lb.GET("/v1/orgs/{org}/github/install/{attempt}", { params: { path: { org: config.org, attempt } } }))).data : void 0;
|
|
29018
29279
|
if (a2 && a2.status !== last) {
|
|
29019
29280
|
last = a2.status;
|
|
29020
29281
|
if (a2.status === "needs_sign_in") explain("GitHub is done. Sign in to Liftbay in that browser to finish linking.", "Waiting for you to sign in to Liftbay in the browser");
|
|
@@ -29023,7 +29284,7 @@ async function waitForInstall(lb, config, attempt) {
|
|
|
29023
29284
|
return { ok: false, message: a2.message };
|
|
29024
29285
|
}
|
|
29025
29286
|
}
|
|
29026
|
-
const s = await status(lb, config);
|
|
29287
|
+
const s = await poll(() => status(lb, config));
|
|
29027
29288
|
if (s.connected) {
|
|
29028
29289
|
spin.stop(`GitHub App installed for ${s.repo}`);
|
|
29029
29290
|
return { ok: true, status: s };
|
|
@@ -29170,9 +29431,91 @@ function pushState(branch) {
|
|
|
29170
29431
|
return { upstream: true, ahead: 0 };
|
|
29171
29432
|
}
|
|
29172
29433
|
}
|
|
29434
|
+
var minutes = (sec) => sec ? sec >= 60 ? `${Math.floor(sec / 60)}m ${sec % 60}s` : `${sec}s` : "";
|
|
29435
|
+
function activity(b) {
|
|
29436
|
+
if (b.status === "queued") return b.engine === "xcode-cloud" ? "Waiting for Xcode Cloud" : "Waiting for a runner";
|
|
29437
|
+
if (b.status === "preparing") return "Starting";
|
|
29438
|
+
if (b.status === "uploading") return "Uploading";
|
|
29439
|
+
return b.phases.find((ph) => ph.status === "running")?.name ?? "Building";
|
|
29440
|
+
}
|
|
29441
|
+
async function followSteps(lb, path2, started) {
|
|
29442
|
+
const tag = (b) => started.length > 1 ? `${b.platform === "ios" ? "iOS" : "Android"} \xB7 ` : "";
|
|
29443
|
+
const shown = new Map(started.map((b) => [b.number, /* @__PURE__ */ new Set()]));
|
|
29444
|
+
const latest = new Map(started.map((b) => [b.number, b]));
|
|
29445
|
+
const runnerSeen = /* @__PURE__ */ new Set();
|
|
29446
|
+
const spin = spinner({ withGuide: false });
|
|
29447
|
+
spin.start(fit(started.map((b) => `${tag(b)}${activity(b)}`).join(" ")));
|
|
29448
|
+
const say = (line) => {
|
|
29449
|
+
spin.clear();
|
|
29450
|
+
process.stdout.write(`${import_picocolors7.default.gray("\u2502")} ${line}
|
|
29451
|
+
`);
|
|
29452
|
+
spin.start(fit(started.map((b) => `${tag(b)}${activity(latest.get(b.number))}`).join(" ")));
|
|
29453
|
+
};
|
|
29454
|
+
for (; ; ) {
|
|
29455
|
+
for (const b of started) {
|
|
29456
|
+
const prev = latest.get(b.number);
|
|
29457
|
+
if (FINISHED.has(prev.status)) continue;
|
|
29458
|
+
const fresh = await poll(() => unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/builds/{number}", { params: { path: { ...path2, number: b.number } } })));
|
|
29459
|
+
latest.set(b.number, fresh);
|
|
29460
|
+
if (fresh.status !== "queued" && !runnerSeen.has(b.number)) {
|
|
29461
|
+
runnerSeen.add(b.number);
|
|
29462
|
+
say(`${import_picocolors7.default.green("\u2714")} ${tag(b)}${fresh.engine === "xcode-cloud" ? "Xcode Cloud started" : "Runner started"}`);
|
|
29463
|
+
}
|
|
29464
|
+
for (const ph of fresh.phases) {
|
|
29465
|
+
if (ph.status === "running" || ph.status === "queued" || shown.get(b.number).has(ph.name)) continue;
|
|
29466
|
+
shown.get(b.number).add(ph.name);
|
|
29467
|
+
const mark = ph.status === "succeeded" ? import_picocolors7.default.green("\u2714") : ph.status === "cancelled" ? import_picocolors7.default.yellow("\u25A0") : import_picocolors7.default.red("\u2716");
|
|
29468
|
+
say(`${mark} ${tag(b)}${ph.name} ${import_picocolors7.default.dim(minutes(ph.durationSec))}`);
|
|
29469
|
+
}
|
|
29470
|
+
}
|
|
29471
|
+
const all = [...latest.values()];
|
|
29472
|
+
if (all.every((b) => FINISHED.has(b.status))) {
|
|
29473
|
+
spin.stop(all.every((b) => b.status === "succeeded") ? "Finished" : "Finished with problems");
|
|
29474
|
+
return all;
|
|
29475
|
+
}
|
|
29476
|
+
spin.message(fit(all.map((b) => `${tag(b)}${FINISHED.has(b.status) ? b.status : activity(b)}`).join(" ")));
|
|
29477
|
+
await sleep3(3e3);
|
|
29478
|
+
}
|
|
29479
|
+
}
|
|
29480
|
+
async function followLogs(lb, path2, started) {
|
|
29481
|
+
const prefix = started.length > 1;
|
|
29482
|
+
return Promise.all(
|
|
29483
|
+
started.map(async (b) => {
|
|
29484
|
+
let after = 0;
|
|
29485
|
+
let phase = null;
|
|
29486
|
+
for (; ; ) {
|
|
29487
|
+
const page = await poll(() => unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/builds/{number}/logs", { params: { path: { ...path2, number: b.number }, query: { after } } })));
|
|
29488
|
+
for (const l2 of page.lines) {
|
|
29489
|
+
const tag = prefix ? import_picocolors7.default.dim(`${b.platform.padEnd(8)}`) : "";
|
|
29490
|
+
if (l2.phase && l2.phase !== phase) {
|
|
29491
|
+
phase = l2.phase;
|
|
29492
|
+
console.log(`${tag}${import_picocolors7.default.cyan(`==> ${phase}`)}`);
|
|
29493
|
+
}
|
|
29494
|
+
console.log(`${tag}${l2.stream === "stderr" ? import_picocolors7.default.red(l2.text) : l2.stream === "system" ? import_picocolors7.default.dim(l2.text) : l2.text}`);
|
|
29495
|
+
}
|
|
29496
|
+
after = page.nextSeq;
|
|
29497
|
+
if (page.complete) break;
|
|
29498
|
+
if (!page.lines.length) {
|
|
29499
|
+
const fresh = await poll(() => unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/builds/{number}", { params: { path: { ...path2, number: b.number } } })));
|
|
29500
|
+
if (FINISHED.has(fresh.status) && fresh.logLineCount <= after) break;
|
|
29501
|
+
await sleep3(2e3);
|
|
29502
|
+
}
|
|
29503
|
+
}
|
|
29504
|
+
return poll(() => unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/builds/{number}", { params: { path: { ...path2, number: b.number } } })));
|
|
29505
|
+
})
|
|
29506
|
+
);
|
|
29507
|
+
}
|
|
29508
|
+
async function printLogTail(lb, path2, b, tagged, count = 30) {
|
|
29509
|
+
const after = Math.max(0, b.logLineCount - count);
|
|
29510
|
+
const page = await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/builds/{number}/logs", { params: { path: { ...path2, number: b.number }, query: { after } } })).catch(() => null);
|
|
29511
|
+
if (!page?.lines.length) return;
|
|
29512
|
+
log.error(`${tagged ? `${b.platform === "ios" ? "iOS" : "Android"} \xB7 ` : ""}build #${b.number} failed. The end of its log:`);
|
|
29513
|
+
console.log(page.lines.slice(-count).map((l2) => l2.stream === "stderr" ? import_picocolors7.default.red(l2.text) : import_picocolors7.default.dim(l2.text)).join("\n"));
|
|
29514
|
+
log.info(`Whole log: ${import_picocolors7.default.cyan(cmd(`liftbay builds logs ${b.number}`))}`);
|
|
29515
|
+
}
|
|
29173
29516
|
var SETUP_FILES = ["liftbay.json", ".github/workflows/liftbay.yml", "app.json", "package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lock", "bun.lockb"];
|
|
29174
29517
|
function git(args) {
|
|
29175
|
-
return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).
|
|
29518
|
+
return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trimEnd();
|
|
29176
29519
|
}
|
|
29177
29520
|
function gitLoud(args) {
|
|
29178
29521
|
try {
|
|
@@ -29182,6 +29525,20 @@ function gitLoud(args) {
|
|
|
29182
29525
|
}
|
|
29183
29526
|
}
|
|
29184
29527
|
async function readyToBuild(branch, interactive) {
|
|
29528
|
+
const wanted = await currentWorkflow();
|
|
29529
|
+
const present = existsSync6(WORKFLOW_PATH) ? readFileSync6(WORKFLOW_PATH, "utf8") : null;
|
|
29530
|
+
if (present !== wanted) {
|
|
29531
|
+
const what = present === null ? `${WORKFLOW_PATH} isn't in this repository` : `${WORKFLOW_PATH} is out of date`;
|
|
29532
|
+
if (!interactive) fail(`${what}. Update it with \`liftbay ci init --yes\`, then commit and push it.`);
|
|
29533
|
+
log.warn(`${what}${present === null ? "" : " (Liftbay changed how builds run)"}.`);
|
|
29534
|
+
if (cancelled(await confirm({ message: present === null ? "Add it?" : "Update it?", initialValue: true }))) {
|
|
29535
|
+
mkdirSync4(dirname3(WORKFLOW_PATH), { recursive: true });
|
|
29536
|
+
writeFileSync6(WORKFLOW_PATH, wanted);
|
|
29537
|
+
log.success(`${present === null ? "Added" : "Updated"} ${WORKFLOW_PATH}`);
|
|
29538
|
+
} else if (present === null) {
|
|
29539
|
+
fail(`Builds need ${WORKFLOW_PATH} on ${branch}. Run \`liftbay ci init\` to add it.`);
|
|
29540
|
+
}
|
|
29541
|
+
}
|
|
29185
29542
|
const changed = git(["status", "--porcelain", "--", ...SETUP_FILES]).split("\n").filter(Boolean).map((l2) => l2.slice(3).replace(/^"|"$/g, ""));
|
|
29186
29543
|
if (changed.some((f) => f === "liftbay.json" || f.startsWith(".github/"))) {
|
|
29187
29544
|
const files = changed.filter((f) => f !== "" && SETUP_FILES.some((s) => f === s || f.startsWith(".github/")));
|
|
@@ -29337,32 +29694,8 @@ async function build(opts) {
|
|
|
29337
29694
|
outro(`Queued. Follow along in the dashboard, or with ${import_picocolors7.default.cyan(cmd("liftbay builds view <number>"))}.`);
|
|
29338
29695
|
return { builds: started.map(summary) };
|
|
29339
29696
|
}
|
|
29340
|
-
const
|
|
29341
|
-
const results
|
|
29342
|
-
started.map(async (b) => {
|
|
29343
|
-
let after = 0;
|
|
29344
|
-
let phase = null;
|
|
29345
|
-
for (; ; ) {
|
|
29346
|
-
const page = await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/builds/{number}/logs", { params: { path: { ...path2, number: b.number }, query: { after } } }));
|
|
29347
|
-
for (const l2 of page.lines) {
|
|
29348
|
-
const tag = prefix ? import_picocolors7.default.dim(`${b.platform.padEnd(8)}`) : "";
|
|
29349
|
-
if (l2.phase && l2.phase !== phase) {
|
|
29350
|
-
phase = l2.phase;
|
|
29351
|
-
console.log(`${tag}${import_picocolors7.default.cyan(`==> ${phase}`)}`);
|
|
29352
|
-
}
|
|
29353
|
-
console.log(`${tag}${l2.stream === "stderr" ? import_picocolors7.default.red(l2.text) : l2.stream === "system" ? import_picocolors7.default.dim(l2.text) : l2.text}`);
|
|
29354
|
-
}
|
|
29355
|
-
after = page.nextSeq;
|
|
29356
|
-
if (page.complete) break;
|
|
29357
|
-
if (!page.lines.length) {
|
|
29358
|
-
const fresh = await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/builds/{number}", { params: { path: { ...path2, number: b.number } } }));
|
|
29359
|
-
if (FINISHED.has(fresh.status) && fresh.logLineCount <= after) break;
|
|
29360
|
-
await sleep3(2e3);
|
|
29361
|
-
}
|
|
29362
|
-
}
|
|
29363
|
-
return unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/builds/{number}", { params: { path: { ...path2, number: b.number } } }));
|
|
29364
|
-
})
|
|
29365
|
-
);
|
|
29697
|
+
const results = opts.logs ? await followLogs(lb, path2, started) : await followSteps(lb, path2, started);
|
|
29698
|
+
for (const r2 of results.filter((r3) => r3.status === "failed")) await printLogTail(lb, path2, r2, started.length > 1);
|
|
29366
29699
|
const out = [];
|
|
29367
29700
|
for (const r2 of results) {
|
|
29368
29701
|
let installUrl = null;
|
|
@@ -29389,7 +29722,7 @@ async function build(opts) {
|
|
|
29389
29722
|
|
|
29390
29723
|
// src/commands/builds.ts
|
|
29391
29724
|
var import_picocolors8 = __toESM(require_picocolors(), 1);
|
|
29392
|
-
import { createWriteStream, mkdirSync as
|
|
29725
|
+
import { createWriteStream, mkdirSync as mkdirSync5 } from "fs";
|
|
29393
29726
|
import { join as join7, resolve as resolve2 } from "path";
|
|
29394
29727
|
import { Readable } from "stream";
|
|
29395
29728
|
import { pipeline } from "stream/promises";
|
|
@@ -29490,7 +29823,7 @@ async function buildsDownload(number, opts) {
|
|
|
29490
29823
|
const ready = b.artifacts.filter((a2) => a2.status === "ready" && (!opts.kind || a2.kind === opts.kind));
|
|
29491
29824
|
if (!ready.length) fail(opts.kind ? `Build #${b.number} has no ready ${opts.kind} artifact` : `Build #${b.number} has no ready artifacts (${b.status})`);
|
|
29492
29825
|
const dir = resolve2(opts.out ?? ".");
|
|
29493
|
-
|
|
29826
|
+
mkdirSync5(dir, { recursive: true });
|
|
29494
29827
|
const files = [];
|
|
29495
29828
|
for (const a2 of ready) {
|
|
29496
29829
|
const link = await unwrap(
|
|
@@ -29577,7 +29910,7 @@ async function xcodeCloudDisconnect() {
|
|
|
29577
29910
|
}
|
|
29578
29911
|
|
|
29579
29912
|
// src/commands/submit.ts
|
|
29580
|
-
import { readFileSync as
|
|
29913
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
29581
29914
|
var import_picocolors10 = __toESM(require_picocolors(), 1);
|
|
29582
29915
|
var TRACKS2 = ["internal", "alpha", "beta", "production"];
|
|
29583
29916
|
var DONE = /* @__PURE__ */ new Set(["succeeded", "failed"]);
|
|
@@ -29607,7 +29940,7 @@ async function submit(opts) {
|
|
|
29607
29940
|
queued.map(async (s) => {
|
|
29608
29941
|
let shown = 0;
|
|
29609
29942
|
for (; ; ) {
|
|
29610
|
-
const d = await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/submissions/{submission}", { params: { path: { ...path2, submission: s.id } } }));
|
|
29943
|
+
const d = await poll(() => unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/submissions/{submission}", { params: { path: { ...path2, submission: s.id } } })));
|
|
29611
29944
|
for (const e of d.events.slice(shown)) log.message(`${import_picocolors10.default.dim(s.platform.padEnd(8))}${e.text}`);
|
|
29612
29945
|
shown = d.events.length;
|
|
29613
29946
|
if (DONE.has(d.status)) return d;
|
|
@@ -29637,7 +29970,7 @@ async function credentialsAndroidCommand(opts) {
|
|
|
29637
29970
|
}
|
|
29638
29971
|
let json2;
|
|
29639
29972
|
try {
|
|
29640
|
-
json2 =
|
|
29973
|
+
json2 = readFileSync7(opts.playKey.replace(/^~/, process.env.HOME ?? "~"), "utf8");
|
|
29641
29974
|
} catch {
|
|
29642
29975
|
fail(`Can't read ${opts.playKey}`);
|
|
29643
29976
|
}
|
|
@@ -29688,7 +30021,7 @@ ${reg.url}`, "Scan with the iPhone's camera, or open the link in Safari on it");
|
|
|
29688
30021
|
const until = Date.parse(reg.expiresAt);
|
|
29689
30022
|
while (Date.now() < until) {
|
|
29690
30023
|
await sleep7(2500);
|
|
29691
|
-
const status2 = await unwrap(lb.GET("/v1/orgs/{org}/devices/registrations/{registration}", { params: { path: { ...org, registration: reg.id } } }));
|
|
30024
|
+
const status2 = await poll(() => unwrap(lb.GET("/v1/orgs/{org}/devices/registrations/{registration}", { params: { path: { ...org, registration: reg.id } } })));
|
|
29692
30025
|
if (status2.device) {
|
|
29693
30026
|
spin.stop(`Registered ${import_picocolors11.default.bold(status2.device.name)} ${import_picocolors11.default.dim(`${status2.device.model ?? ""} ${status2.device.udid}`)}`);
|
|
29694
30027
|
await nextStep(lb);
|
|
@@ -29717,171 +30050,16 @@ async function deviceRemove(id) {
|
|
|
29717
30050
|
}
|
|
29718
30051
|
|
|
29719
30052
|
// src/commands/setup.ts
|
|
29720
|
-
import { existsSync as
|
|
29721
|
-
import { dirname as
|
|
29722
|
-
var
|
|
29723
|
-
|
|
29724
|
-
// src/generated/workflow.ts
|
|
29725
|
-
var WORKFLOW_YAML = `# Liftbay builds on your own GitHub Actions minutes.
|
|
29726
|
-
# Liftbay starts this workflow; it does nothing on your own pushes or pull requests. With this file on the
|
|
29727
|
-
# default branch, Liftbay uses workflow_dispatch. Otherwise (a branch you're trying out), Liftbay points a
|
|
29728
|
-
# temporary liftbay/build/<platform>/<build id> branch at your commit, which runs this file from that commit,
|
|
29729
|
-
# and deletes it once the build has started.
|
|
29730
|
-
# The build gets no repository secrets: the runner exchanges this run's GitHub OIDC token for a
|
|
29731
|
-
# one-time Liftbay job token, checked against this repository and this workflow file.
|
|
29732
|
-
# Docs: https://github.com/liftbay/runner
|
|
29733
|
-
name: Liftbay build
|
|
29734
|
-
run-name: Liftbay build \${{ inputs.build_id || github.ref_name }}
|
|
29735
|
-
|
|
29736
|
-
on:
|
|
29737
|
-
workflow_dispatch:
|
|
29738
|
-
inputs:
|
|
29739
|
-
build_id:
|
|
29740
|
-
description: Liftbay build id
|
|
29741
|
-
required: true
|
|
29742
|
-
type: string
|
|
29743
|
-
platform:
|
|
29744
|
-
description: Platform
|
|
29745
|
-
required: true
|
|
29746
|
-
type: choice
|
|
29747
|
-
options: [android, ios]
|
|
29748
|
-
api_url:
|
|
29749
|
-
description: Liftbay API
|
|
29750
|
-
required: false
|
|
29751
|
-
type: string
|
|
29752
|
-
default: https://api.liftbay.dev
|
|
29753
|
-
push:
|
|
29754
|
-
branches: ["liftbay/build/**"]
|
|
29755
|
-
|
|
29756
|
-
permissions:
|
|
29757
|
-
contents: read
|
|
29758
|
-
id-token: write
|
|
29759
|
-
|
|
29760
|
-
env:
|
|
29761
|
-
# Pinned runner release. The archive's SHA-256 is checked before the runner starts.
|
|
29762
|
-
# Release notes and provenance: https://github.com/liftbay/runner/releases/tag/v0.1.2
|
|
29763
|
-
RUNNER_VERSION: "0.1.2"
|
|
29764
|
-
RUNNER_SHA256_LINUX_AMD64: "9b37bfe534b6dc40f81182bdb9dc46461a672f2c28f048743dc8f13f630909b1"
|
|
29765
|
-
RUNNER_SHA256_DARWIN_ARM64: "301bf09358a47b48df0cdbd6f60edbac1227e4d7f61e037959d4009ed6ae7f27"
|
|
29766
|
-
|
|
29767
|
-
jobs:
|
|
29768
|
-
android:
|
|
29769
|
-
if: inputs.platform == 'android' || startsWith(github.ref_name, 'liftbay/build/android/')
|
|
29770
|
-
runs-on: ubuntu-latest
|
|
29771
|
-
timeout-minutes: 60
|
|
29772
|
-
steps:
|
|
29773
|
-
- uses: actions/checkout@v4
|
|
29774
|
-
|
|
29775
|
-
- uses: actions/setup-java@v4
|
|
29776
|
-
with:
|
|
29777
|
-
distribution: temurin
|
|
29778
|
-
java-version: "17"
|
|
29779
|
-
|
|
29780
|
-
- uses: actions/setup-node@v4
|
|
29781
|
-
with:
|
|
29782
|
-
node-version: "22"
|
|
29783
|
-
|
|
29784
|
-
- name: Cache JavaScript dependencies
|
|
29785
|
-
uses: actions/cache@v4
|
|
29786
|
-
with:
|
|
29787
|
-
path: |
|
|
29788
|
-
node_modules
|
|
29789
|
-
~/.npm
|
|
29790
|
-
~/.cache/yarn
|
|
29791
|
-
~/.local/share/pnpm/store
|
|
29792
|
-
~/.bun/install/cache
|
|
29793
|
-
key: js-\${{ runner.os }}-\${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb') }}
|
|
29794
|
-
restore-keys: js-\${{ runner.os }}-
|
|
29795
|
-
|
|
29796
|
-
- name: Cache Gradle
|
|
29797
|
-
uses: actions/cache@v4
|
|
29798
|
-
with:
|
|
29799
|
-
path: |
|
|
29800
|
-
~/.gradle/caches
|
|
29801
|
-
~/.gradle/wrapper
|
|
29802
|
-
key: gradle-\${{ runner.os }}-\${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb', 'android/**/*.gradle*', 'android/gradle/wrapper/gradle-wrapper.properties') }}
|
|
29803
|
-
restore-keys: gradle-\${{ runner.os }}-
|
|
29804
|
-
|
|
29805
|
-
- name: Install the Liftbay runner
|
|
29806
|
-
env:
|
|
29807
|
-
# Bring your own runner: set the repository variables LIFTBAY_RUNNER_URL (a
|
|
29808
|
-
# liftbay-runner tar.gz you built) and LIFTBAY_RUNNER_SHA256.
|
|
29809
|
-
RUNNER_URL: \${{ vars.LIFTBAY_RUNNER_URL }}
|
|
29810
|
-
RUNNER_SHA256: \${{ vars.LIFTBAY_RUNNER_SHA256 || env.RUNNER_SHA256_LINUX_AMD64 }}
|
|
29811
|
-
run: |
|
|
29812
|
-
set -euo pipefail
|
|
29813
|
-
url="\${RUNNER_URL:-https://github.com/liftbay/runner/releases/download/v\${RUNNER_VERSION}/liftbay-runner_\${RUNNER_VERSION}_linux_amd64.tar.gz}"
|
|
29814
|
-
curl -fsSL --retry 3 -o "$RUNNER_TEMP/liftbay-runner.tar.gz" "$url"
|
|
29815
|
-
echo "\${RUNNER_SHA256} $RUNNER_TEMP/liftbay-runner.tar.gz" | sha256sum -c -
|
|
29816
|
-
tar -xzf "$RUNNER_TEMP/liftbay-runner.tar.gz" -C "$RUNNER_TEMP" liftbay-runner
|
|
29817
|
-
"$RUNNER_TEMP/liftbay-runner" version
|
|
29818
|
-
|
|
29819
|
-
- name: Build
|
|
29820
|
-
env:
|
|
29821
|
-
LIFTBAY_BUILD_ID: \${{ inputs.build_id }}
|
|
29822
|
-
LIFTBAY_API_URL: \${{ inputs.api_url || vars.LIFTBAY_API_URL || 'https://api.liftbay.dev' }}
|
|
29823
|
-
# On a liftbay/build/<platform>/<build id> branch, the build id is the branch name's last part.
|
|
29824
|
-
run: 'LIFTBAY_BUILD_ID="\${LIFTBAY_BUILD_ID:-\${GITHUB_REF_NAME##*/}}" "$RUNNER_TEMP/liftbay-runner" run'
|
|
29825
|
-
|
|
29826
|
-
ios:
|
|
29827
|
-
if: inputs.platform == 'ios' || startsWith(github.ref_name, 'liftbay/build/ios/')
|
|
29828
|
-
runs-on: macos-15
|
|
29829
|
-
timeout-minutes: 90
|
|
29830
|
-
steps:
|
|
29831
|
-
- uses: actions/checkout@v4
|
|
29832
|
-
|
|
29833
|
-
- uses: maxim-lobanov/setup-xcode@v1
|
|
29834
|
-
with:
|
|
29835
|
-
xcode-version: latest-stable
|
|
29836
|
-
|
|
29837
|
-
- uses: actions/setup-node@v4
|
|
29838
|
-
with:
|
|
29839
|
-
node-version: "22"
|
|
29840
|
-
|
|
29841
|
-
- name: Cache JavaScript dependencies
|
|
29842
|
-
uses: actions/cache@v4
|
|
29843
|
-
with:
|
|
29844
|
-
path: |
|
|
29845
|
-
node_modules
|
|
29846
|
-
~/.npm
|
|
29847
|
-
~/Library/Caches/Yarn
|
|
29848
|
-
~/Library/pnpm/store
|
|
29849
|
-
~/.bun/install/cache
|
|
29850
|
-
key: js-\${{ runner.os }}-\${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb') }}
|
|
29851
|
-
restore-keys: js-\${{ runner.os }}-
|
|
29852
|
-
|
|
29853
|
-
# Only the download caches: a restored ios/Pods would make expo prebuild see a half project.
|
|
29854
|
-
- name: Cache CocoaPods
|
|
29855
|
-
uses: actions/cache@v4
|
|
29856
|
-
with:
|
|
29857
|
-
path: |
|
|
29858
|
-
~/Library/Caches/CocoaPods
|
|
29859
|
-
~/.cocoapods
|
|
29860
|
-
key: pods-\${{ runner.os }}-\${{ hashFiles('ios/Podfile.lock', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb') }}
|
|
29861
|
-
restore-keys: pods-\${{ runner.os }}-
|
|
29862
|
-
|
|
29863
|
-
- name: Install the Liftbay runner
|
|
29864
|
-
env:
|
|
29865
|
-
RUNNER_URL: \${{ vars.LIFTBAY_RUNNER_URL_MACOS }}
|
|
29866
|
-
RUNNER_SHA256: \${{ vars.LIFTBAY_RUNNER_SHA256_MACOS || env.RUNNER_SHA256_DARWIN_ARM64 }}
|
|
29867
|
-
run: |
|
|
29868
|
-
set -euo pipefail
|
|
29869
|
-
url="\${RUNNER_URL:-https://github.com/liftbay/runner/releases/download/v\${RUNNER_VERSION}/liftbay-runner_\${RUNNER_VERSION}_darwin_arm64.tar.gz}"
|
|
29870
|
-
curl -fsSL --retry 3 -o "$RUNNER_TEMP/liftbay-runner.tar.gz" "$url"
|
|
29871
|
-
echo "\${RUNNER_SHA256} $RUNNER_TEMP/liftbay-runner.tar.gz" | shasum -a 256 -c -
|
|
29872
|
-
tar -xzf "$RUNNER_TEMP/liftbay-runner.tar.gz" -C "$RUNNER_TEMP" liftbay-runner
|
|
29873
|
-
"$RUNNER_TEMP/liftbay-runner" version
|
|
30053
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
|
|
30054
|
+
import { dirname as dirname4 } from "path";
|
|
30055
|
+
var import_picocolors14 = __toESM(require_picocolors(), 1);
|
|
29874
30056
|
|
|
29875
|
-
|
|
29876
|
-
|
|
29877
|
-
|
|
29878
|
-
LIFTBAY_API_URL: \${{ inputs.api_url || vars.LIFTBAY_API_URL || 'https://api.liftbay.dev' }}
|
|
29879
|
-
# On a liftbay/build/<platform>/<build id> branch, the build id is the branch name's last part.
|
|
29880
|
-
run: 'LIFTBAY_BUILD_ID="\${LIFTBAY_BUILD_ID:-\${GITHUB_REF_NAME##*/}}" "$RUNNER_TEMP/liftbay-runner" run'
|
|
29881
|
-
`;
|
|
30057
|
+
// src/commands/answers.ts
|
|
30058
|
+
import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
|
|
30059
|
+
var import_picocolors13 = __toESM(require_picocolors(), 1);
|
|
29882
30060
|
|
|
29883
30061
|
// src/commands/env.ts
|
|
29884
|
-
import { existsSync as
|
|
30062
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
29885
30063
|
import { basename as basename2, join as join8, resolve as resolve3 } from "path";
|
|
29886
30064
|
var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
29887
30065
|
var VISIBILITIES = ["plain", "sensitive", "secret"];
|
|
@@ -29925,10 +30103,10 @@ function parseVisibility(v) {
|
|
|
29925
30103
|
var slugify2 = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "default";
|
|
29926
30104
|
function readEasProfiles(cwd = process.cwd()) {
|
|
29927
30105
|
const path2 = join8(cwd, "eas.json");
|
|
29928
|
-
if (!
|
|
30106
|
+
if (!existsSync7(path2)) return null;
|
|
29929
30107
|
let eas;
|
|
29930
30108
|
try {
|
|
29931
|
-
eas = JSON.parse(
|
|
30109
|
+
eas = JSON.parse(readFileSync8(path2, "utf8"));
|
|
29932
30110
|
} catch {
|
|
29933
30111
|
fail("eas.json isn't valid JSON");
|
|
29934
30112
|
}
|
|
@@ -30049,7 +30227,7 @@ async function envImport(files, opts) {
|
|
|
30049
30227
|
const results = [];
|
|
30050
30228
|
for (const file2 of files) {
|
|
30051
30229
|
const abs = resolve3(file2.replace(/^~/, process.env.HOME ?? "~"));
|
|
30052
|
-
if (!
|
|
30230
|
+
if (!existsSync7(abs)) fail(`No file at ${file2}`);
|
|
30053
30231
|
if (basename2(abs) === "eas.json") {
|
|
30054
30232
|
const profiles = readEasProfiles(resolve3(abs, ".."));
|
|
30055
30233
|
if (!profiles.length) fail("eas.json has no build profiles");
|
|
@@ -30067,7 +30245,7 @@ async function envImport(files, opts) {
|
|
|
30067
30245
|
}
|
|
30068
30246
|
continue;
|
|
30069
30247
|
}
|
|
30070
|
-
const { vars, invalid } = parseEnv(
|
|
30248
|
+
const { vars, invalid } = parseEnv(readFileSync8(abs, "utf8"));
|
|
30071
30249
|
if (invalid.length) log.warn(`${file2}: skipped unreadable line${invalid.length > 1 ? "s" : ""} ${invalid.join(", ")}`);
|
|
30072
30250
|
const { slug } = await pickEnvironment(lb, path2, opts.environment, file2);
|
|
30073
30251
|
const r2 = await importVariables(lb, path2, slug, vars, { visibility, overwrite });
|
|
@@ -30086,13 +30264,165 @@ async function envExport(opts) {
|
|
|
30086
30264
|
`);
|
|
30087
30265
|
return { environment: slug, content };
|
|
30088
30266
|
}
|
|
30089
|
-
if (
|
|
30090
|
-
|
|
30267
|
+
if (existsSync7(opts.out) && !opts.force) fail(`${opts.out} exists. Pass --force to overwrite it.`);
|
|
30268
|
+
writeFileSync7(opts.out, `${content.trimEnd()}
|
|
30091
30269
|
`, { mode: 384 });
|
|
30092
30270
|
log.success(`Wrote ${opts.out} ${import_picocolors12.default.dim("(secret values are not exported)")}`);
|
|
30093
30271
|
return { environment: slug, file: resolve3(opts.out) };
|
|
30094
30272
|
}
|
|
30095
30273
|
|
|
30274
|
+
// src/commands/answers.ts
|
|
30275
|
+
var KEYS = ["org", "newOrg", "project", "newProject", "platforms", "iosEngine", "eas", "ota", "runtimePolicy", "envFiles"];
|
|
30276
|
+
function readAnswers(path2) {
|
|
30277
|
+
if (!existsSync8(path2)) fail(`No answers file at ${path2}`);
|
|
30278
|
+
let a2;
|
|
30279
|
+
try {
|
|
30280
|
+
a2 = JSON.parse(readFileSync9(path2, "utf8"));
|
|
30281
|
+
} catch {
|
|
30282
|
+
fail(`${path2} isn't valid JSON`);
|
|
30283
|
+
}
|
|
30284
|
+
const problems = [];
|
|
30285
|
+
const unknown = Object.keys(a2).filter((k2) => !KEYS.includes(k2));
|
|
30286
|
+
if (unknown.length) problems.push(`unknown field${unknown.length > 1 ? "s" : ""} ${unknown.join(", ")} (allowed: ${KEYS.join(", ")})`);
|
|
30287
|
+
const str = (k2) => a2[k2] !== void 0 && (typeof a2[k2] !== "string" || !a2[k2].trim()) && problems.push(`${k2} must be a non-empty string`);
|
|
30288
|
+
const bool = (k2) => a2[k2] !== void 0 && typeof a2[k2] !== "boolean" && problems.push(`${k2} must be true or false`);
|
|
30289
|
+
const oneOf = (k2, values) => a2[k2] !== void 0 && !values.includes(a2[k2]) && problems.push(`${k2} must be ${values.join(" or ")}`);
|
|
30290
|
+
["org", "newOrg", "project", "newProject"].forEach(str);
|
|
30291
|
+
["eas", "ota"].forEach(bool);
|
|
30292
|
+
oneOf("iosEngine", ["github-actions", "xcode-cloud"]);
|
|
30293
|
+
oneOf("runtimePolicy", ["appVersion", "fingerprint"]);
|
|
30294
|
+
if (a2.org && a2.newOrg) problems.push("give org or newOrg, not both");
|
|
30295
|
+
if (a2.project && a2.newProject) problems.push("give project or newProject, not both");
|
|
30296
|
+
if (a2.platforms !== void 0 && (!Array.isArray(a2.platforms) || !a2.platforms.length || !a2.platforms.every((x) => x === "ios" || x === "android"))) {
|
|
30297
|
+
problems.push(`platforms must be a list of "ios" and/or "android"`);
|
|
30298
|
+
}
|
|
30299
|
+
if (a2.envFiles !== void 0) {
|
|
30300
|
+
const ok = typeof a2.envFiles === "object" && a2.envFiles !== null && Object.values(a2.envFiles).every((v) => Array.isArray(v) && v.every((f) => typeof f === "string"));
|
|
30301
|
+
if (!ok) problems.push(`envFiles must look like { "production": [".env.production"] }`);
|
|
30302
|
+
else for (const f of Object.values(a2.envFiles).flat()) if (!existsSync8(f)) problems.push(`envFiles: no file at ${f}`);
|
|
30303
|
+
}
|
|
30304
|
+
if (problems.length) fail(`${path2}:
|
|
30305
|
+
${problems.join("\n ")}`);
|
|
30306
|
+
return a2;
|
|
30307
|
+
}
|
|
30308
|
+
function answersToOptions(a2) {
|
|
30309
|
+
return {
|
|
30310
|
+
org: a2.org,
|
|
30311
|
+
newOrg: a2.newOrg,
|
|
30312
|
+
project: a2.project,
|
|
30313
|
+
projectName: a2.newProject,
|
|
30314
|
+
platforms: a2.platforms?.join(","),
|
|
30315
|
+
iosEngine: a2.iosEngine,
|
|
30316
|
+
eas: a2.eas,
|
|
30317
|
+
ota: a2.ota,
|
|
30318
|
+
runtimePolicy: a2.runtimePolicy,
|
|
30319
|
+
yes: true
|
|
30320
|
+
};
|
|
30321
|
+
}
|
|
30322
|
+
async function initQuestions() {
|
|
30323
|
+
const app = await detectApp();
|
|
30324
|
+
const linked = readConfig();
|
|
30325
|
+
const signedIn = !!(process.env.LIFTBAY_TOKEN || readCredentials());
|
|
30326
|
+
let orgs = [];
|
|
30327
|
+
const projects = {};
|
|
30328
|
+
if (signedIn) {
|
|
30329
|
+
const lb = client();
|
|
30330
|
+
orgs = await unwrap(lb.GET("/v1/orgs")).catch(() => []);
|
|
30331
|
+
for (const o of orgs) projects[o.slug] = await unwrap(lb.GET("/v1/orgs/{org}/projects", { params: { path: { org: o.slug } } })).catch(() => []);
|
|
30332
|
+
}
|
|
30333
|
+
const eas = readEasProfiles();
|
|
30334
|
+
const envFiles = readdirSync3(".").filter((f) => /^\.env(\..+)?$/.test(f) && !/\.(example|sample|template)$/.test(f));
|
|
30335
|
+
const policy = readAppJson()?.expo?.runtimeVersion?.policy;
|
|
30336
|
+
const questions = [];
|
|
30337
|
+
if (!linked) {
|
|
30338
|
+
questions.push(
|
|
30339
|
+
orgs.length ? {
|
|
30340
|
+
id: "org",
|
|
30341
|
+
question: "Which Liftbay organization should own this app?",
|
|
30342
|
+
type: "choice",
|
|
30343
|
+
choices: orgs.map((o) => ({ value: o.slug, label: o.name })),
|
|
30344
|
+
default: orgs.length === 1 ? orgs[0].slug : void 0,
|
|
30345
|
+
note: "Or answer newOrg with a name to create another organization."
|
|
30346
|
+
} : {
|
|
30347
|
+
id: "newOrg",
|
|
30348
|
+
question: "What should the new Liftbay organization be called?",
|
|
30349
|
+
type: "text",
|
|
30350
|
+
note: signedIn ? "This account has no organization yet; init creates it." : "Not signed in yet, so existing organizations can't be listed. Sign in first to see them."
|
|
30351
|
+
},
|
|
30352
|
+
{
|
|
30353
|
+
id: "project",
|
|
30354
|
+
question: "Use an existing Liftbay project, or create a new one?",
|
|
30355
|
+
type: "choice",
|
|
30356
|
+
choices: Object.values(projects).flat().map((pr) => ({ value: pr.slug, label: pr.name })),
|
|
30357
|
+
default: void 0,
|
|
30358
|
+
note: `For a new project, answer newProject with its name instead (suggested: "${app.name}").`
|
|
30359
|
+
}
|
|
30360
|
+
);
|
|
30361
|
+
}
|
|
30362
|
+
questions.push({ id: "platforms", question: "Which platforms should Liftbay build?", type: "multi", choices: [{ value: "ios", label: "iOS" }, { value: "android", label: "Android" }], default: ["ios", "android"] });
|
|
30363
|
+
questions.push({
|
|
30364
|
+
id: "iosEngine",
|
|
30365
|
+
question: "Where should iOS store builds run by default?",
|
|
30366
|
+
type: "choice",
|
|
30367
|
+
when: "platforms includes ios",
|
|
30368
|
+
choices: [
|
|
30369
|
+
{ value: "github-actions", label: "GitHub Actions (macOS runners on your GitHub minutes; ready now)" },
|
|
30370
|
+
{ value: "xcode-cloud", label: "Xcode Cloud (25 free hours a month with the Apple Developer Program; one-time setup after iOS signing)" }
|
|
30371
|
+
],
|
|
30372
|
+
default: "github-actions"
|
|
30373
|
+
});
|
|
30374
|
+
if (eas?.length) {
|
|
30375
|
+
questions.push({
|
|
30376
|
+
id: "eas",
|
|
30377
|
+
question: `Use eas.json's build profiles (${eas.map((e) => e.name).join(", ")}), their channels and env variables?`,
|
|
30378
|
+
type: "boolean",
|
|
30379
|
+
default: true
|
|
30380
|
+
});
|
|
30381
|
+
}
|
|
30382
|
+
questions.push({ id: "ota", question: "Set up over-the-air updates (expo-updates)?", type: "boolean", default: true });
|
|
30383
|
+
questions.push({
|
|
30384
|
+
id: "runtimePolicy",
|
|
30385
|
+
question: "Which installed apps can receive an update?",
|
|
30386
|
+
type: "choice",
|
|
30387
|
+
when: "ota is true",
|
|
30388
|
+
choices: [
|
|
30389
|
+
{ value: "appVersion", label: "Same app version (bump the version when native code changes)" },
|
|
30390
|
+
{ value: "fingerprint", label: "Same native fingerprint (computed from native code and config)" }
|
|
30391
|
+
],
|
|
30392
|
+
default: policy === "fingerprint" ? "fingerprint" : "appVersion"
|
|
30393
|
+
});
|
|
30394
|
+
if (envFiles.length) {
|
|
30395
|
+
questions.push({
|
|
30396
|
+
id: "envFiles",
|
|
30397
|
+
question: `Which of these files should go into which environment? ${envFiles.join(", ")}`,
|
|
30398
|
+
type: "files",
|
|
30399
|
+
default: {},
|
|
30400
|
+
note: 'Like { "production": [".env.production"], "preview": [".env.preview"] }. Files like .env or .env.local often hold local development values: ask before sending them.'
|
|
30401
|
+
});
|
|
30402
|
+
}
|
|
30403
|
+
const example = {
|
|
30404
|
+
...linked ? {} : orgs.length ? { org: orgs[0].slug } : { newOrg: "Acme" },
|
|
30405
|
+
...linked ? {} : { newProject: app.name },
|
|
30406
|
+
platforms: ["ios", "android"],
|
|
30407
|
+
iosEngine: "github-actions",
|
|
30408
|
+
...eas?.length ? { eas: true } : {},
|
|
30409
|
+
ota: true,
|
|
30410
|
+
runtimePolicy: "appVersion"
|
|
30411
|
+
};
|
|
30412
|
+
const next = `Ask the person these questions, write their answers to a JSON file, then run: ${cmd("liftbay init --answers <file> --json")}`;
|
|
30413
|
+
intro(import_picocolors13.default.bgCyan(import_picocolors13.default.black(" liftbay init --questions ")));
|
|
30414
|
+
log.info(`${import_picocolors13.default.bold(app.name)} \xB7 ${app.framework === "expo" ? "Expo" : "React Native"}${app.bundleId ? ` \xB7 ${app.bundleId}` : ""}${linked ? ` \xB7 linked to ${linked.org}/${linked.project}` : ""}`);
|
|
30415
|
+
if (!signedIn) log.warn(`Not signed in: ${cmd("liftbay login")} first to list organizations and projects.`);
|
|
30416
|
+
for (const q of questions) {
|
|
30417
|
+
const choices = q.choices?.length ? `
|
|
30418
|
+
${q.choices.map((c2) => ` ${import_picocolors13.default.cyan(c2.value)} ${import_picocolors13.default.dim(c2.label)}`).join("\n")}` : "";
|
|
30419
|
+
log.message(`${import_picocolors13.default.bold(q.id)}${q.when ? import_picocolors13.default.dim(` (if ${q.when})`) : ""}: ${q.question}${choices}${q.note ? `
|
|
30420
|
+
${import_picocolors13.default.dim(q.note)}` : ""}`);
|
|
30421
|
+
}
|
|
30422
|
+
outro(next);
|
|
30423
|
+
return { app: { name: app.name, framework: app.framework, bundleId: app.bundleId, androidPackage: app.androidPackage }, linked: linked ? { org: linked.org, project: linked.project } : null, signedIn, questions, example, next };
|
|
30424
|
+
}
|
|
30425
|
+
|
|
30096
30426
|
// src/commands/setup.ts
|
|
30097
30427
|
var DEFAULT_PROFILES = {
|
|
30098
30428
|
development: { environment: "development", channel: "development", distribution: "internal" },
|
|
@@ -30135,11 +30465,11 @@ async function ciInit(app, config, opts) {
|
|
|
30135
30465
|
})
|
|
30136
30466
|
);
|
|
30137
30467
|
}
|
|
30138
|
-
if (platforms.includes("ios") && !app.bundleId) log.warn(`No iOS bundle identifier found. Set ${
|
|
30139
|
-
if (platforms.includes("android") && !app.androidPackage) log.warn(`No Android package found. Set ${
|
|
30468
|
+
if (platforms.includes("ios") && !app.bundleId) log.warn(`No iOS bundle identifier found. Set ${import_picocolors14.default.cyan("expo.ios.bundleIdentifier")} in app.json before the first iOS build.`);
|
|
30469
|
+
if (platforms.includes("android") && !app.androidPackage) log.warn(`No Android package found. Set ${import_picocolors14.default.cyan("expo.android.package")} in app.json before the first Android build.`);
|
|
30140
30470
|
const path2 = { org: config.org, project: config.project };
|
|
30141
30471
|
const repo = await githubRepo();
|
|
30142
|
-
if (!repo) log.warn(`No GitHub remote found. Builds run on GitHub Actions: push this app to GitHub, then run ${
|
|
30472
|
+
if (!repo) log.warn(`No GitHub remote found. Builds run on GitHub Actions: push this app to GitHub, then run ${import_picocolors14.default.cyan(cmd("liftbay ci init"))} again.`);
|
|
30143
30473
|
await unwrap(
|
|
30144
30474
|
lb.PATCH("/v1/orgs/{org}/projects/{project}", {
|
|
30145
30475
|
params: { path: path2 },
|
|
@@ -30167,22 +30497,22 @@ async function ciInit(app, config, opts) {
|
|
|
30167
30497
|
printImport(r2);
|
|
30168
30498
|
imported.push(r2);
|
|
30169
30499
|
}
|
|
30170
|
-
log.info(
|
|
30171
|
-
log.info(
|
|
30500
|
+
log.info(import_picocolors14.default.dim("EXPO_PUBLIC_* values are plain (they're in the app bundle); names like *_TOKEN or *_SECRET are secret; the rest sensitive."));
|
|
30501
|
+
log.info(import_picocolors14.default.dim(`Variables kept on expo.dev (EAS environment variables) aren't in eas.json: ${import_picocolors14.default.cyan("npx eas env:pull --environment production")}, then ${import_picocolors14.default.cyan(cmd("liftbay env import .env.local --environment production"))}.`));
|
|
30172
30502
|
}
|
|
30173
30503
|
const profiles = Object.fromEntries(Object.entries(base).map(([name, prof]) => [name, { ...prof, platforms }]));
|
|
30174
30504
|
if (iosEngine) await unwrap(lb.PATCH("/v1/orgs/{org}/projects/{project}", { params: { path: path2 }, body: { iosEngine } }));
|
|
30175
30505
|
const next = { ...config, build: { profiles, triggers: config.build?.triggers ?? [] } };
|
|
30176
30506
|
writeConfig(next);
|
|
30177
30507
|
note(
|
|
30178
|
-
Object.entries(profiles).map(([name, prof]) => `${
|
|
30508
|
+
Object.entries(profiles).map(([name, prof]) => `${import_picocolors14.default.bold(name.padEnd(12))} env ${prof.environment.padEnd(12)} ${prof.distribution === "store" ? "store" : "internal"} \xB7 ${prof.platforms.join(", ")}`).join("\n") + (iosEngine ? `
|
|
30179
30509
|
|
|
30180
30510
|
iOS store builds run on ${iosEngine === "xcode-cloud" ? "Xcode Cloud" : "GitHub Actions"} (project setting)` : ""),
|
|
30181
30511
|
"Build profiles"
|
|
30182
30512
|
);
|
|
30183
30513
|
if (iosEngine === "xcode-cloud") {
|
|
30184
30514
|
log.info(
|
|
30185
|
-
`Xcode Cloud makes iOS store builds (TestFlight); internal and simulator builds stay on GitHub Actions. It needs iOS signing first: ${
|
|
30515
|
+
`Xcode Cloud makes iOS store builds (TestFlight); internal and simulator builds stay on GitHub Actions. It needs iOS signing first: ${import_picocolors14.default.cyan(cmd("liftbay credentials ios"))}, then ${import_picocolors14.default.cyan(cmd("liftbay xcode-cloud connect"))}.`
|
|
30186
30516
|
);
|
|
30187
30517
|
}
|
|
30188
30518
|
lastImport = imported;
|
|
@@ -30197,12 +30527,11 @@ async function githubStep() {
|
|
|
30197
30527
|
} catch (e) {
|
|
30198
30528
|
if (!(e instanceof CliError)) throw e;
|
|
30199
30529
|
log.warn(e.message);
|
|
30200
|
-
log.info(`Finish any time with ${
|
|
30530
|
+
log.info(`Finish any time with ${import_picocolors14.default.cyan(cmd("liftbay github connect"))}; ${cmd("liftbay build")} checks it too.`);
|
|
30201
30531
|
return { error: e.message };
|
|
30202
30532
|
}
|
|
30203
30533
|
}
|
|
30204
30534
|
var lastImport = [];
|
|
30205
|
-
var WORKFLOW_PATH = ".github/workflows/liftbay.yml";
|
|
30206
30535
|
async function githubRepo() {
|
|
30207
30536
|
try {
|
|
30208
30537
|
const url = (await output("git", ["remote", "get-url", "origin"])).trim();
|
|
@@ -30212,16 +30541,17 @@ async function githubRepo() {
|
|
|
30212
30541
|
}
|
|
30213
30542
|
}
|
|
30214
30543
|
async function writeWorkflow(opts) {
|
|
30215
|
-
const
|
|
30216
|
-
|
|
30544
|
+
const wanted = await currentWorkflow();
|
|
30545
|
+
const current = existsSync9(WORKFLOW_PATH) ? readFileSync10(WORKFLOW_PATH, "utf8") : null;
|
|
30546
|
+
if (current === wanted) return;
|
|
30217
30547
|
if (current !== null) {
|
|
30218
30548
|
const replace = opts.yes || isInteractive() && cancelled(await confirm({ message: `${WORKFLOW_PATH} differs from the current Liftbay workflow. Replace it?` }));
|
|
30219
30549
|
if (!replace) return log.info(`Kept your ${WORKFLOW_PATH}.`);
|
|
30220
30550
|
}
|
|
30221
|
-
|
|
30222
|
-
|
|
30223
|
-
log.success(`Wrote ${
|
|
30224
|
-
log.info(
|
|
30551
|
+
mkdirSync6(dirname4(WORKFLOW_PATH), { recursive: true });
|
|
30552
|
+
writeFileSync8(WORKFLOW_PATH, wanted);
|
|
30553
|
+
log.success(`Wrote ${import_picocolors14.default.cyan(WORKFLOW_PATH)}. Commit and push it: Liftbay starts it for every build, on your GitHub Actions minutes.`);
|
|
30554
|
+
log.info(import_picocolors14.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."));
|
|
30225
30555
|
}
|
|
30226
30556
|
var pluginSpec = () => process.env.LIFTBAY_EXPO_PLUGIN_SPEC ?? "@liftbay/expo-plugin";
|
|
30227
30557
|
async function addDependency(app, spec, label) {
|
|
@@ -30260,12 +30590,12 @@ async function otaInit(app, config, opts) {
|
|
|
30260
30590
|
if (!env2) fail(`Environment ${prof.environment} not found; run \`liftbay ci init\` first`);
|
|
30261
30591
|
if (!existing) {
|
|
30262
30592
|
await unwrap(lb.POST("/v1/orgs/{org}/projects/{project}/channels", { params: { path: path2 }, body: { name: prof.channel, environmentId: env2.id, protocols: ["expo"] } }));
|
|
30263
|
-
log.success(`Created channel ${
|
|
30593
|
+
log.success(`Created channel ${import_picocolors14.default.bold(prof.channel)} \u2192 ${prof.environment}`);
|
|
30264
30594
|
} else if (!existing.protocols.includes("expo")) {
|
|
30265
30595
|
await unwrap(
|
|
30266
30596
|
lb.PATCH("/v1/orgs/{org}/projects/{project}/channels/{channel}", { params: { path: { ...path2, channel: prof.channel } }, body: { protocols: [...existing.protocols, "expo"] } })
|
|
30267
30597
|
);
|
|
30268
|
-
log.success(`Enabled expo-updates on channel ${
|
|
30598
|
+
log.success(`Enabled expo-updates on channel ${import_picocolors14.default.bold(prof.channel)}`);
|
|
30269
30599
|
}
|
|
30270
30600
|
}
|
|
30271
30601
|
channels2 = await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/channels", { params: { path: path2 } }));
|
|
@@ -30285,24 +30615,24 @@ runtimeVersion: { "policy": "${policy}" }`, "Add to your app config");
|
|
|
30285
30615
|
else plugins.push(entry);
|
|
30286
30616
|
expo.runtimeVersion = { policy };
|
|
30287
30617
|
writeAppJson(appJson);
|
|
30288
|
-
log.success(`Updated ${
|
|
30618
|
+
log.success(`Updated ${import_picocolors14.default.cyan("app.json")}: Liftbay plugin, runtimeVersion policy "${policy}"`);
|
|
30289
30619
|
}
|
|
30290
30620
|
const next = { ...config, updates: { client: "expo-updates", runtimeVersion: { policy } } };
|
|
30291
30621
|
writeConfig(next);
|
|
30292
30622
|
return next;
|
|
30293
30623
|
}
|
|
30294
30624
|
async function ciInitCommand(opts) {
|
|
30295
|
-
intro(
|
|
30625
|
+
intro(import_picocolors14.default.bgCyan(import_picocolors14.default.black(" liftbay ci init ")));
|
|
30296
30626
|
await ensureSignedIn(opts);
|
|
30297
30627
|
const app = await detectApp();
|
|
30298
30628
|
const config = await ensureLinked(app, opts);
|
|
30299
30629
|
const next = await ciInit(app, config, opts);
|
|
30300
30630
|
const github2 = await githubStep();
|
|
30301
|
-
outro(`Builds are configured. Start one with ${
|
|
30631
|
+
outro(`Builds are configured. Start one with ${import_picocolors14.default.cyan(cmd("liftbay build"))}`);
|
|
30302
30632
|
return { config: next, imported: lastImport, github: github2 };
|
|
30303
30633
|
}
|
|
30304
30634
|
async function otaInitCommand(opts) {
|
|
30305
|
-
intro(
|
|
30635
|
+
intro(import_picocolors14.default.bgCyan(import_picocolors14.default.black(" liftbay ota init ")));
|
|
30306
30636
|
await ensureSignedIn(opts);
|
|
30307
30637
|
const app = await detectApp();
|
|
30308
30638
|
const config = await ensureLinked(app, opts);
|
|
@@ -30313,37 +30643,49 @@ async function otaInitCommand(opts) {
|
|
|
30313
30643
|
var nextStepsOta = () => `OTA updates are configured.
|
|
30314
30644
|
|
|
30315
30645
|
1. Build a new binary once (the update URL and channel are baked in)
|
|
30316
|
-
2. Ship JS changes with ${
|
|
30646
|
+
2. Ship JS changes with ${import_picocolors14.default.cyan(cmd("liftbay update publish --channel production"))}`;
|
|
30317
30647
|
async function initCommand(opts) {
|
|
30318
|
-
|
|
30648
|
+
if (opts.questions) return initQuestions();
|
|
30649
|
+
let envFiles;
|
|
30650
|
+
if (opts.answers) {
|
|
30651
|
+
const answers = readAnswers(opts.answers);
|
|
30652
|
+
Object.assign(opts, answersToOptions(answers));
|
|
30653
|
+
envFiles = answers.envFiles;
|
|
30654
|
+
disablePrompts();
|
|
30655
|
+
}
|
|
30656
|
+
intro(import_picocolors14.default.bgCyan(import_picocolors14.default.black(" liftbay init ")));
|
|
30319
30657
|
await ensureSignedIn(opts);
|
|
30320
30658
|
const app = await detectApp();
|
|
30321
30659
|
log.info(
|
|
30322
|
-
`${
|
|
30660
|
+
`${import_picocolors14.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}` : ""}`
|
|
30323
30661
|
);
|
|
30324
30662
|
let config = await ensureLinked(app, opts);
|
|
30325
|
-
log.step(
|
|
30663
|
+
log.step(import_picocolors14.default.bold("1/3 Builds"));
|
|
30326
30664
|
config = await ciInit(app, config, opts);
|
|
30327
|
-
log.step(
|
|
30665
|
+
log.step(import_picocolors14.default.bold("2/3 OTA updates"));
|
|
30328
30666
|
const wantOta = opts.ota ?? (opts.yes || !isInteractive() ? true : cancelled(await confirm({ message: "Set up over-the-air updates with expo-updates?", initialValue: true })));
|
|
30329
30667
|
if (wantOta) config = await otaInit(await detectApp(), config, opts);
|
|
30330
|
-
else log.info(`Skipped. Run ${
|
|
30331
|
-
|
|
30668
|
+
else log.info(`Skipped. Run ${import_picocolors14.default.cyan(cmd("liftbay ota init"))} any time.`);
|
|
30669
|
+
const imported = [];
|
|
30670
|
+
for (const [environment, files] of Object.entries(envFiles ?? {})) {
|
|
30671
|
+
if (files.length) imported.push(...(await envImport(files, { environment })).imported);
|
|
30672
|
+
}
|
|
30673
|
+
log.step(import_picocolors14.default.bold("3/3 GitHub"));
|
|
30332
30674
|
const github2 = await githubStep();
|
|
30333
30675
|
outro(
|
|
30334
30676
|
[
|
|
30335
|
-
`Done. ${
|
|
30677
|
+
`Done. ${import_picocolors14.default.dim(`${dashboardUrl()}/${config.org}/${config.project}`)}`,
|
|
30336
30678
|
"",
|
|
30337
30679
|
" Next:",
|
|
30338
|
-
` 1. Build: ${
|
|
30339
|
-
...wantOta ? [` 2. Update: ${
|
|
30340
|
-
...cmd("liftbay") !== "liftbay" ? ["",
|
|
30680
|
+
` 1. Build: ${import_picocolors14.default.cyan(cmd("liftbay build"))} ${import_picocolors14.default.dim("(offers to commit the setup and push this branch)")}`,
|
|
30681
|
+
...wantOta ? [` 2. Update: ${import_picocolors14.default.cyan(cmd("liftbay update publish --channel preview"))} ${import_picocolors14.default.dim("(after a build from this setup is installed)")}`] : [],
|
|
30682
|
+
...cmd("liftbay") !== "liftbay" ? ["", import_picocolors14.default.dim(` Tip: ${import_picocolors14.default.cyan("npm i -g liftbay")} to type ${import_picocolors14.default.cyan("liftbay")} instead of ${import_picocolors14.default.cyan("npx liftbay")}.`)] : []
|
|
30341
30683
|
].join("\n")
|
|
30342
30684
|
);
|
|
30343
30685
|
return {
|
|
30344
30686
|
github: github2,
|
|
30345
30687
|
config,
|
|
30346
|
-
imported: lastImport,
|
|
30688
|
+
imported: [...lastImport, ...imported],
|
|
30347
30689
|
url: `${dashboardUrl()}/${config.org}/${config.project}`,
|
|
30348
30690
|
next: ["Commit liftbay.json, .github/workflows/liftbay.yml and app.json, and push", "liftbay build --profile preview", ...wantOta ? ["liftbay update publish --channel production"] : []]
|
|
30349
30691
|
};
|
|
@@ -30352,17 +30694,17 @@ async function initCommand(opts) {
|
|
|
30352
30694
|
// src/commands/update.ts
|
|
30353
30695
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
30354
30696
|
import { createHash as createHash4 } from "crypto";
|
|
30355
|
-
import { mkdtempSync, readFileSync as
|
|
30697
|
+
import { mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync4 } from "fs";
|
|
30356
30698
|
import { tmpdir } from "os";
|
|
30357
30699
|
import { join as join9 } from "path";
|
|
30358
|
-
var
|
|
30700
|
+
var import_picocolors16 = __toESM(require_picocolors(), 1);
|
|
30359
30701
|
|
|
30360
30702
|
// src/commands/signing.ts
|
|
30361
30703
|
import { createPublicKey, createSign, randomBytes as randomBytes2 } from "crypto";
|
|
30362
|
-
import { chmodSync as chmodSync4, existsSync as
|
|
30363
|
-
import { dirname as
|
|
30704
|
+
import { chmodSync as chmodSync4, existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
|
|
30705
|
+
import { dirname as dirname5, relative } from "path";
|
|
30364
30706
|
var import_node_forge = __toESM(require_lib2(), 1);
|
|
30365
|
-
var
|
|
30707
|
+
var import_picocolors15 = __toESM(require_picocolors(), 1);
|
|
30366
30708
|
var DEFAULT_PRIVATE_KEY = "keys/liftbay-private-key.pem";
|
|
30367
30709
|
var DEFAULT_PUBLIC_KEY = "certs/liftbay-public-key.pem";
|
|
30368
30710
|
var DEFAULT_CERTIFICATE = "certs/liftbay-certificate.pem";
|
|
@@ -30392,18 +30734,18 @@ function generateSigningCertificate(commonName, years = 10) {
|
|
|
30392
30734
|
var signRsaSha256 = (privateKeyPem, data) => createSign("RSA-SHA256").update(data).sign(privateKeyPem, "base64");
|
|
30393
30735
|
function loadPrivateKey(path2) {
|
|
30394
30736
|
const read = (file2, label) => {
|
|
30395
|
-
if (!
|
|
30396
|
-
return { pem:
|
|
30737
|
+
if (!existsSync10(file2)) fail(`No private key at ${file2} (${label})`);
|
|
30738
|
+
return { pem: readFileSync11(file2, "utf8"), source: file2 };
|
|
30397
30739
|
};
|
|
30398
30740
|
if (path2) return read(path2, "--private-key");
|
|
30399
30741
|
const env2 = process.env.LIFTBAY_UPDATE_PRIVATE_KEY;
|
|
30400
30742
|
if (env2) return env2.includes("PRIVATE KEY") ? { pem: env2.replace(/\\n/g, "\n"), source: "LIFTBAY_UPDATE_PRIVATE_KEY" } : read(env2, "LIFTBAY_UPDATE_PRIVATE_KEY");
|
|
30401
|
-
return
|
|
30743
|
+
return existsSync10(DEFAULT_PRIVATE_KEY) ? { pem: readFileSync11(DEFAULT_PRIVATE_KEY, "utf8"), source: DEFAULT_PRIVATE_KEY } : null;
|
|
30402
30744
|
}
|
|
30403
30745
|
function ensureIgnored(entry) {
|
|
30404
|
-
const current =
|
|
30746
|
+
const current = existsSync10(".gitignore") ? readFileSync11(".gitignore", "utf8") : "";
|
|
30405
30747
|
if (current.split("\n").some((l2) => l2.trim() === entry || l2.trim() === entry.replace(/\/$/, ""))) return false;
|
|
30406
|
-
|
|
30748
|
+
writeFileSync9(".gitignore", `${current}${current && !current.endsWith("\n") ? "\n" : ""}
|
|
30407
30749
|
# Liftbay update signing key: never commit it
|
|
30408
30750
|
${entry}
|
|
30409
30751
|
`);
|
|
@@ -30425,7 +30767,7 @@ async function signingInit(opts) {
|
|
|
30425
30767
|
const config = requireConfig();
|
|
30426
30768
|
const lb = client();
|
|
30427
30769
|
const path2 = { org: config.org, project: config.project };
|
|
30428
|
-
intro(
|
|
30770
|
+
intro(import_picocolors15.default.bgCyan(import_picocolors15.default.black(" liftbay update signing init ")));
|
|
30429
30771
|
const { signing: signing2 } = await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/updates/signing", { params: { path: path2 } }));
|
|
30430
30772
|
if (signing2 && !opts.force) {
|
|
30431
30773
|
fail(
|
|
@@ -30434,16 +30776,16 @@ async function signingInit(opts) {
|
|
|
30434
30776
|
}
|
|
30435
30777
|
const privateKeyPath = opts.privateKey ?? DEFAULT_PRIVATE_KEY;
|
|
30436
30778
|
const certificatePath = opts.certificate ?? DEFAULT_CERTIFICATE;
|
|
30437
|
-
if (
|
|
30779
|
+
if (existsSync10(privateKeyPath) && !opts.force) fail(`${privateKeyPath} already exists. Pass --force to overwrite it, or --private-key for another path.`);
|
|
30438
30780
|
const generated = generateSigningCertificate(opts.commonName ?? config.project);
|
|
30439
|
-
for (const dir of /* @__PURE__ */ new Set([
|
|
30440
|
-
|
|
30781
|
+
for (const dir of /* @__PURE__ */ new Set([dirname5(privateKeyPath), dirname5(certificatePath), dirname5(DEFAULT_PUBLIC_KEY)])) mkdirSync7(dir, { recursive: true });
|
|
30782
|
+
writeFileSync9(privateKeyPath, generated.privateKeyPem, { mode: 384 });
|
|
30441
30783
|
chmodSync4(privateKeyPath, 384);
|
|
30442
|
-
|
|
30443
|
-
|
|
30444
|
-
if (ensureIgnored(`${
|
|
30445
|
-
log.success(`Private key: ${
|
|
30446
|
-
Certificate: ${
|
|
30784
|
+
writeFileSync9(certificatePath, generated.certificatePem);
|
|
30785
|
+
writeFileSync9(DEFAULT_PUBLIC_KEY, generated.publicKeyPem);
|
|
30786
|
+
if (ensureIgnored(`${dirname5(relative(".", privateKeyPath))}/`)) log.info(`Added ${import_picocolors15.default.cyan(`${dirname5(privateKeyPath)}/`)} to .gitignore`);
|
|
30787
|
+
log.success(`Private key: ${import_picocolors15.default.cyan(privateKeyPath)} (stays on this machine)
|
|
30788
|
+
Certificate: ${import_picocolors15.default.cyan(certificatePath)} (commit it; it's embedded in your app)`);
|
|
30447
30789
|
await unwrap(
|
|
30448
30790
|
lb.PUT("/v1/orgs/{org}/projects/{project}/updates/signing", {
|
|
30449
30791
|
params: { path: path2 },
|
|
@@ -30451,7 +30793,7 @@ async function signingInit(opts) {
|
|
|
30451
30793
|
})
|
|
30452
30794
|
);
|
|
30453
30795
|
log.success("Certificate saved to Liftbay");
|
|
30454
|
-
if (configureAppJson(certificatePath)) log.success(`Updated ${
|
|
30796
|
+
if (configureAppJson(certificatePath)) log.success(`Updated ${import_picocolors15.default.cyan("app.json")}: the Liftbay plugin embeds the certificate in your app`);
|
|
30455
30797
|
else note(`plugins: [["@liftbay/expo-plugin", { "codeSigningCertificate": "./${certificatePath}" }]]`, "Add to your app config");
|
|
30456
30798
|
const channels2 = await unwrap(lb.GET("/v1/orgs/{org}/projects/{project}/channels", { params: { path: path2 } }));
|
|
30457
30799
|
const signedOnly = !!channels2.length && (opts.yes || !isInteractive() || cancelled(await confirm({ message: "Require signed updates on every channel? Unsigned publishes will be refused.", initialValue: true })));
|
|
@@ -30464,12 +30806,12 @@ async function signingInit(opts) {
|
|
|
30464
30806
|
note(
|
|
30465
30807
|
[
|
|
30466
30808
|
`1. Rebuild your apps once: the certificate is part of the binary.`,
|
|
30467
|
-
`2. ${
|
|
30468
|
-
`3. In CI, store the private key as a secret named ${
|
|
30809
|
+
`2. ${import_picocolors15.default.cyan(cmd("liftbay update publish"))} signs automatically with ${privateKeyPath}.`,
|
|
30810
|
+
`3. In CI, store the private key as a secret named ${import_picocolors15.default.cyan("LIFTBAY_UPDATE_PRIVATE_KEY")}.`,
|
|
30469
30811
|
`4. Back the key up somewhere safe. If it's lost, apps need a rebuild with a new certificate.`,
|
|
30470
30812
|
``,
|
|
30471
|
-
`hot-updater: set ${
|
|
30472
|
-
`CodePush: set ${
|
|
30813
|
+
`hot-updater: set ${import_picocolors15.default.cyan("HOT_UPDATER_PUBLIC_KEY")} (iOS Info.plist) / ${import_picocolors15.default.cyan("com.hotupdater.PUBLIC_KEY")} (Android) to ${DEFAULT_PUBLIC_KEY}.`,
|
|
30814
|
+
`CodePush: set ${import_picocolors15.default.cyan("CodePushPublicKey")} (iOS) / ${import_picocolors15.default.cyan("CodePushPublicKey")} string resource (Android) to ${DEFAULT_PUBLIC_KEY}.`
|
|
30473
30815
|
].join("\n"),
|
|
30474
30816
|
"Next"
|
|
30475
30817
|
);
|
|
@@ -30500,13 +30842,13 @@ var contentTypes = {
|
|
|
30500
30842
|
mp4: "video/mp4"
|
|
30501
30843
|
};
|
|
30502
30844
|
function file(dir, rel, virtualPath, contentType) {
|
|
30503
|
-
const data =
|
|
30845
|
+
const data = readFileSync12(join9(dir, rel));
|
|
30504
30846
|
return { path: virtualPath, abs: join9(dir, rel), sha256: createHash4("sha256").update(data).digest("hex"), sizeBytes: data.length, contentType };
|
|
30505
30847
|
}
|
|
30506
30848
|
async function exportBundles(platforms) {
|
|
30507
30849
|
const out = mkdtempSync(join9(tmpdir(), "liftbay-export-"));
|
|
30508
30850
|
await run("npx", ["expo", "export", "--output-dir", out, "--dump-assetmap", ...platforms.flatMap((pl) => ["--platform", pl])]);
|
|
30509
|
-
const meta = JSON.parse(
|
|
30851
|
+
const meta = JSON.parse(readFileSync12(join9(out, "metadata.json"), "utf8"));
|
|
30510
30852
|
const bundles = {};
|
|
30511
30853
|
for (const pl of platforms) {
|
|
30512
30854
|
const m = meta.fileMetadata[pl];
|
|
@@ -30545,7 +30887,7 @@ async function publish(opts) {
|
|
|
30545
30887
|
return null;
|
|
30546
30888
|
}
|
|
30547
30889
|
})();
|
|
30548
|
-
intro(
|
|
30890
|
+
intro(import_picocolors16.default.bgCyan(import_picocolors16.default.black(" liftbay update publish ")));
|
|
30549
30891
|
if (!platforms.every((x) => x === "ios" || x === "android")) fail("--platform must be ios, android or all");
|
|
30550
30892
|
log.step(`Exporting ${platforms.join(" and ")} bundles`);
|
|
30551
30893
|
const { out, bundles } = await exportBundles(platforms);
|
|
@@ -30567,13 +30909,13 @@ async function publish(opts) {
|
|
|
30567
30909
|
for (const m of prep.missing) {
|
|
30568
30910
|
const f = bySha.get(m.sha256);
|
|
30569
30911
|
spin.message(fit(`Uploading ${++done}/${prep.missing.length}: ${f.path}`));
|
|
30570
|
-
const res = await fetch(m.uploadUrl, { method: "PUT", body:
|
|
30912
|
+
const res = await fetch(m.uploadUrl, { method: "PUT", body: readFileSync12(f.abs), headers: { "Content-Type": f.contentType } });
|
|
30571
30913
|
if (!res.ok) fail(`Upload failed for ${f.path} (${res.status})`);
|
|
30572
30914
|
}
|
|
30573
30915
|
spin.stop(`${all.length} files: ${prep.missing.length} uploaded, ${prep.alreadyStored} already stored`);
|
|
30574
30916
|
const key = loadPrivateKey(opts.privateKey);
|
|
30575
30917
|
const published = [];
|
|
30576
|
-
if (key) log.info(`Signing with ${
|
|
30918
|
+
if (key) log.info(`Signing with ${import_picocolors16.default.cyan(key.source)}`);
|
|
30577
30919
|
for (const [runtime, group] of groups) {
|
|
30578
30920
|
const body = {
|
|
30579
30921
|
message,
|
|
@@ -30599,11 +30941,11 @@ async function publish(opts) {
|
|
|
30599
30941
|
if (notice) log.warn(notice);
|
|
30600
30942
|
published.push({ id: release.id, channel, runtimeVersion: runtime, platforms: group, rolloutPercent: release.rolloutPercent, signed: release.signed, message, notice });
|
|
30601
30943
|
log.success(
|
|
30602
|
-
`Published to ${
|
|
30944
|
+
`Published to ${import_picocolors16.default.bold(channel)} \xB7 runtime ${runtime} \xB7 ${group.join(", ")}${release.rolloutPercent < 100 ? ` \xB7 ${release.rolloutPercent}% rollout` : ""}${release.signed ? " \xB7 signed" : ""}`
|
|
30603
30945
|
);
|
|
30604
30946
|
}
|
|
30605
30947
|
const url = `${dashboardUrl()}/${config.org}/${config.project}/updates/${channel}`;
|
|
30606
|
-
outro(
|
|
30948
|
+
outro(import_picocolors16.default.dim(url));
|
|
30607
30949
|
return { releases: published, url };
|
|
30608
30950
|
} finally {
|
|
30609
30951
|
rmSync4(out, { recursive: true, force: true });
|
|
@@ -30615,8 +30957,8 @@ async function list(opts) {
|
|
|
30615
30957
|
const releases = await unwrap(client().GET("/v1/orgs/{org}/projects/{project}/channels/{channel}/releases", { params: { path: { org: config.org, project: config.project, channel } } }));
|
|
30616
30958
|
if (!releases.length) console.log(`No releases on ${channel} yet.`);
|
|
30617
30959
|
for (const r2 of releases.slice(0, 20)) {
|
|
30618
|
-
const status2 = r2.status === "active" ?
|
|
30619
|
-
console.log(`${new Date(r2.createdAt).toLocaleString().padEnd(24)} ${status2.padEnd(22)} ${String(r2.rolloutPercent).padStart(3)}% ${
|
|
30960
|
+
const status2 = r2.status === "active" ? import_picocolors16.default.green("active") : r2.status === "rolled_back" ? import_picocolors16.default.red("rolled back") : import_picocolors16.default.dim("superseded");
|
|
30961
|
+
console.log(`${new Date(r2.createdAt).toLocaleString().padEnd(24)} ${status2.padEnd(22)} ${String(r2.rolloutPercent).padStart(3)}% ${import_picocolors16.default.dim(r2.runtimeVersion.padEnd(10))} ${r2.message}`);
|
|
30620
30962
|
}
|
|
30621
30963
|
return { channel, releases: releases.slice(0, 20) };
|
|
30622
30964
|
}
|
|
@@ -30631,9 +30973,9 @@ async function rollback(opts) {
|
|
|
30631
30973
|
const { path: path2, active } = await activeRelease(opts.channel ?? "production");
|
|
30632
30974
|
const request = client().POST("/v1/orgs/{org}/projects/{project}/channels/{channel}/releases/{release}/rollback", { params: { path: { ...path2, release: active.id } } });
|
|
30633
30975
|
const res = await unwrap(request);
|
|
30634
|
-
console.log(`${
|
|
30976
|
+
console.log(`${import_picocolors16.default.green("\u2714")} Rolled back "${active.message}". ${res.active ? "The previous release is active again." : "Devices return to the bundle built into the app."}`);
|
|
30635
30977
|
const notice = (await request).response.headers.get("Liftbay-Notice");
|
|
30636
|
-
if (notice) console.log(`${
|
|
30978
|
+
if (notice) console.log(`${import_picocolors16.default.yellow("!")} ${notice}`);
|
|
30637
30979
|
return { rolledBack: { id: active.id, message: active.message }, active: res.active, notice };
|
|
30638
30980
|
}
|
|
30639
30981
|
async function rollout(percent, opts) {
|
|
@@ -30641,18 +30983,18 @@ async function rollout(percent, opts) {
|
|
|
30641
30983
|
const r2 = await unwrap(
|
|
30642
30984
|
client().PATCH("/v1/orgs/{org}/projects/{project}/channels/{channel}/releases/{release}/rollout", { params: { path: { ...path2, release: active.id } }, body: { rolloutPercent: Number(percent) } })
|
|
30643
30985
|
);
|
|
30644
|
-
console.log(`${
|
|
30986
|
+
console.log(`${import_picocolors16.default.green("\u2714")} "${r2.message}" now rolls out to ${r2.rolloutPercent}%`);
|
|
30645
30987
|
return { id: r2.id, message: r2.message, rolloutPercent: r2.rolloutPercent };
|
|
30646
30988
|
}
|
|
30647
30989
|
async function channels() {
|
|
30648
30990
|
const config = requireConfig();
|
|
30649
30991
|
const list2 = await unwrap(client().GET("/v1/orgs/{org}/projects/{project}/channels", { params: { path: { org: config.org, project: config.project } } }));
|
|
30650
|
-
for (const c2 of list2) console.log(`${
|
|
30992
|
+
for (const c2 of list2) console.log(`${import_picocolors16.default.bold(c2.name.padEnd(16))} env ${c2.environmentSlug.padEnd(12)} ${c2.protocols.join(", ").padEnd(32)} ${import_picocolors16.default.dim(c2.currentRelease?.message ?? "no active release")}`);
|
|
30651
30993
|
return { channels: list2 };
|
|
30652
30994
|
}
|
|
30653
30995
|
|
|
30654
30996
|
// src/index.ts
|
|
30655
|
-
var program2 = new Command("liftbay").description("Build, ship and update React Native apps").version("0.
|
|
30997
|
+
var program2 = new Command("liftbay").description("Build, ship and update React Native apps").version("0.7.0").option("--json", "print the result as JSON on stdout (progress goes to stderr; never prompts). Also LIFTBAY_JSON=1").hook("preAction", () => {
|
|
30656
30998
|
if (program2.opts().json || process.env.LIFTBAY_JSON === "1") enableJson();
|
|
30657
30999
|
});
|
|
30658
31000
|
var wrap = (fn) => async (...args) => {
|
|
@@ -30663,13 +31005,15 @@ var wrap = (fn) => async (...args) => {
|
|
|
30663
31005
|
}
|
|
30664
31006
|
};
|
|
30665
31007
|
var setupOptions = (cmd2) => cmd2.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("--ios-engine <engine>", "project setting: where iOS store builds run, github-actions or xcode-cloud").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");
|
|
30666
|
-
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));
|
|
31008
|
+
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)").option("--email <address>", "sign in with a code sent to this email, in the terminal (creates the account the first time)").option("--code <code>", "with --email: the 6-digit code from the email").action(wrap(login));
|
|
30667
31009
|
program2.command("logout").description("Sign out and remove the saved token").action(wrap(logout));
|
|
30668
31010
|
program2.command("whoami").description("Show the signed-in user and organizations").action(wrap(whoami));
|
|
30669
|
-
setupOptions(
|
|
31011
|
+
setupOptions(
|
|
31012
|
+
program2.command("init").description("Set up this app: link the project, builds, then OTA updates").option("--no-ota", "skip OTA updates").option("--new-org <name>", "create an organization with this name").option("--project-name <name>", "create the project with this name").option("--questions", "list every question init would ask, with the choices for this app, and exit (for agents: interview the person)").option("--answers <file>", "run with the answers in this JSON file, without any prompts (see --questions)")
|
|
31013
|
+
).action(wrap(initCommand));
|
|
30670
31014
|
var ci = program2.command("ci").description("Builds");
|
|
30671
31015
|
setupOptions(ci.command("init").description("Set up build profiles, environments and app identifiers")).action(wrap(ciInitCommand));
|
|
30672
|
-
program2.command("build").description("Build on your GitHub Actions (Android on Linux, iOS on macOS) and follow the logs").option("--platform <platform>", "ios, android or all (asked in a terminal; otherwise the profile's platforms)").option("--profile <name>", "build profile from liftbay.json (asked in a terminal; otherwise production)").option("--ref <ref>", "branch, tag or commit to build (default: current branch)").option("-m, --message <text>", "build message").option("--simulator", "iOS: build for the simulator instead of a signed .ipa").option("--no-wait", "queue and exit without following the logs").option("--submit", "submit store builds when they succeed: TestFlight, or a Google Play track").option("--track <track>", "Play track for --submit: internal, alpha, beta or production").option("--engine <engine>", "where this iOS store build runs: github-actions or xcode-cloud (asked in a terminal; otherwise the project's setting)").action(wrap(build));
|
|
31016
|
+
program2.command("build").description("Build on your GitHub Actions (Android on Linux, iOS on macOS) and follow the logs").option("--platform <platform>", "ios, android or all (asked in a terminal; otherwise the profile's platforms)").option("--profile <name>", "build profile from liftbay.json (asked in a terminal; otherwise production)").option("--ref <ref>", "branch, tag or commit to build (default: current branch)").option("-m, --message <text>", "build message").option("--simulator", "iOS: build for the simulator instead of a signed .ipa").option("--no-wait", "queue and exit without following the build").option("--logs", "show the whole build log as it runs, instead of just the steps").option("--submit", "submit store builds when they succeed: TestFlight, or a Google Play track").option("--track <track>", "Play track for --submit: internal, alpha, beta or production").option("--engine <engine>", "where this iOS store build runs: github-actions or xcode-cloud (asked in a terminal; otherwise the project's setting)").action(wrap(build));
|
|
30673
31017
|
var builds = program2.command("builds").description("Look at builds: list, status, logs, artifacts");
|
|
30674
31018
|
builds.command("list").description("Recent builds, newest first").option("--platform <platform>", "ios or android").option("--status <list>", "comma-separated: queued, preparing, running, uploading, succeeded, failed, cancelled").option("--limit <n>", "how many", "20").action(wrap(buildsList));
|
|
30675
31019
|
builds.command("view <number>").description("Status, phases and artifacts of one build").action(wrap(buildsView));
|