@shipvo/cli 0.1.4 → 0.1.6

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.
Files changed (93) hide show
  1. package/README.md +3 -0
  2. package/dist/adopt.js +1 -2
  3. package/dist/agents.d.ts +29 -0
  4. package/dist/agents.js +183 -0
  5. package/dist/approvals.d.ts +9 -0
  6. package/dist/approvals.js +106 -0
  7. package/dist/audit.d.ts +13 -0
  8. package/dist/audit.js +92 -0
  9. package/dist/autohalt.d.ts +1 -1
  10. package/dist/autohalt.js +28 -16
  11. package/dist/bsdiff.d.ts +4 -0
  12. package/dist/bsdiff.js +465 -0
  13. package/dist/ci-comment.d.ts +38 -0
  14. package/dist/ci-comment.js +169 -0
  15. package/dist/ci-compare.d.ts +10 -0
  16. package/dist/ci-compare.js +25 -0
  17. package/dist/ci-shots.d.ts +9 -0
  18. package/dist/ci-shots.js +81 -0
  19. package/dist/ci.d.ts +11 -0
  20. package/dist/ci.js +100 -0
  21. package/dist/cli.js +51 -1
  22. package/dist/config.d.ts +15 -0
  23. package/dist/config.js +68 -13
  24. package/dist/doctor.d.ts +2 -0
  25. package/dist/doctor.js +88 -8
  26. package/dist/eas.d.ts +29 -0
  27. package/dist/eas.js +258 -0
  28. package/dist/ensure-playwright.d.ts +25 -0
  29. package/dist/ensure-playwright.js +69 -0
  30. package/dist/explore-issues.d.ts +12 -0
  31. package/dist/explore-issues.js +50 -0
  32. package/dist/explore.d.ts +160 -0
  33. package/dist/explore.js +496 -0
  34. package/dist/flows.d.ts +31 -0
  35. package/dist/flows.js +264 -0
  36. package/dist/help.d.ts +2 -2
  37. package/dist/help.js +37 -7
  38. package/dist/hermes.d.ts +10 -0
  39. package/dist/hermes.js +50 -0
  40. package/dist/init.d.ts +1 -0
  41. package/dist/init.js +71 -19
  42. package/dist/json-out.d.ts +53 -0
  43. package/dist/json-out.js +91 -0
  44. package/dist/keys-json.d.ts +1 -0
  45. package/dist/keys-json.js +71 -0
  46. package/dist/login.js +27 -11
  47. package/dist/lumi.d.ts +104 -0
  48. package/dist/lumi.js +364 -0
  49. package/dist/migrate.d.ts +27 -0
  50. package/dist/migrate.js +521 -0
  51. package/dist/policy.d.ts +2 -16
  52. package/dist/policy.js +2 -18
  53. package/dist/preview-recorder.d.ts +1 -0
  54. package/dist/preview-recorder.js +62 -0
  55. package/dist/preview.d.ts +4 -3
  56. package/dist/preview.js +114 -15
  57. package/dist/promote.d.ts +1 -1
  58. package/dist/promote.js +63 -14
  59. package/dist/publish.d.ts +14 -2
  60. package/dist/publish.js +131 -56
  61. package/dist/qr.d.ts +9 -0
  62. package/dist/qr.js +213 -0
  63. package/dist/report-md.d.ts +27 -0
  64. package/dist/report-md.js +58 -0
  65. package/dist/rotation.d.ts +57 -0
  66. package/dist/rotation.js +157 -0
  67. package/dist/run-bridge.d.ts +7 -0
  68. package/dist/run-bridge.js +728 -0
  69. package/dist/run-net.d.ts +39 -0
  70. package/dist/run-net.js +266 -0
  71. package/dist/run-protocol.d.ts +32 -0
  72. package/dist/run-protocol.js +22 -0
  73. package/dist/run-ws.d.ts +8 -0
  74. package/dist/run-ws.js +89 -0
  75. package/dist/run.d.ts +109 -0
  76. package/dist/run.js +385 -0
  77. package/dist/signing.js +13 -0
  78. package/dist/sim.d.ts +4 -0
  79. package/dist/sim.js +25 -7
  80. package/dist/smoke.d.ts +28 -0
  81. package/dist/smoke.js +117 -0
  82. package/dist/spawn-bin.d.ts +11 -0
  83. package/dist/spawn-bin.js +21 -0
  84. package/dist/status.d.ts +71 -1
  85. package/dist/status.js +162 -5
  86. package/dist/store.d.ts +36 -0
  87. package/dist/store.js +266 -3
  88. package/dist/templates/capacitor/github-actions/build.yml +122 -0
  89. package/dist/templates/github-action/shipvo-check.yml +18 -0
  90. package/dist/test.d.ts +46 -0
  91. package/dist/test.js +253 -0
  92. package/package.json +7 -4
  93. package/skills/shipvo-cli/SKILL.md +31 -0
package/dist/store.js CHANGED
@@ -1,12 +1,23 @@
1
- import { mkdirSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { createInterface } from "node:readline/promises";
2
3
  import { join } from "node:path";
3
- import { deriveStoreDetails, renderDataSafetyCsv, renderPrivacyPolicy } from "@shipvo/store";
4
+ import { createSign } from "node:crypto";
5
+ import { checkScreenshot, checkSet, SCREENSHOT_SETS, deriveStoreDetails, renderDataSafetyCsv, renderPrivacyPolicy, routeStore, SHIPVO_TESTERS_GROUP, SHIPVO_TESTERS_JOIN_URL } from "@shipvo/store";
4
6
  import { args, loadConfig } from "./config.js";
7
+ import { startSim } from "./explore.js";
5
8
  /** `shipvo store details --dry-run [--out dir]`: print the answers, write two files. Nothing is uploaded; no credentials are read. */
6
9
  export async function store(argv) {
7
10
  const [sub, ...rest] = argv;
11
+ if (sub === "setup")
12
+ return setup(rest);
13
+ if (sub === "status")
14
+ return status(rest);
15
+ if (sub === "testers")
16
+ return testers(rest);
17
+ if (sub === "screenshots")
18
+ return screenshots(rest);
8
19
  if (sub !== "details") {
9
- console.error("Usage: shipvo store details --dry-run [--out dir]");
20
+ console.error("Usage: shipvo store details --dry-run [--out dir] | store setup | store status | store screenshots | store testers");
10
21
  process.exit(2);
11
22
  }
12
23
  const a = args(rest);
@@ -32,3 +43,255 @@ export async function store(argv) {
32
43
  console.log(` - ${u.question}`);
33
44
  console.log(`\nWrote ${join(out, "data-safety.csv")} and ${join(out, "privacy-policy.md")}. Nothing was uploaded.`);
34
45
  }
46
+ const PLAN_FILE = "store-plan.json";
47
+ function fail(msg) { console.error(msg); process.exit(2); }
48
+ function yesNo(flag, v) {
49
+ if (v === undefined)
50
+ return undefined;
51
+ if (["yes", "true"].includes(v))
52
+ return true;
53
+ if (["no", "false"].includes(v))
54
+ return false;
55
+ return fail(`shipvo store setup: --${flag} must be yes or no, not "${v}".`);
56
+ }
57
+ async function ask(rl, q, def) {
58
+ const r = (await rl.question(`${q} [${def ? "Y/n" : "y/N"}] `)).trim().toLowerCase();
59
+ return r === "" ? def : r.startsWith("y");
60
+ }
61
+ function describe(title, r, marks, withStatus) {
62
+ console.log(`${title}: ${r.account} account. ${r.accountNote}`);
63
+ let next;
64
+ for (const s of r.steps) {
65
+ const m = marks[s.id] ?? "todo";
66
+ if (!next && m !== "done")
67
+ next = s;
68
+ console.log(` ${withStatus ? `[${m}] ` : "- "}${s.id}: ${s.title}${s.wait ? ` (wait: ${s.wait})` : ""}`);
69
+ }
70
+ if (!withStatus)
71
+ return;
72
+ if (!next)
73
+ console.log(" Done: this store is live.");
74
+ else if ((marks[next.id] ?? "todo") === "waiting")
75
+ console.log(` Next: wait. ${next.wait ?? next.title}. Then run \`shipvo store status --mark ${next.id}\`.`);
76
+ else
77
+ console.log(` Next: ${next.action} Then run \`shipvo store status --mark ${next.id}\`.`);
78
+ }
79
+ function printPlan(plan, withStatus) {
80
+ const r = routeStore(plan.answers);
81
+ describe("Google Play", r.play, plan.marks, withStatus);
82
+ console.log("");
83
+ describe("App Store", r.apple, plan.marks, withStatus);
84
+ if (!withStatus)
85
+ for (const n of r.notes)
86
+ console.log(`\n${n}`);
87
+ }
88
+ /** `shipvo store setup [--company yes|no] [--android-now] [--play-account-before-2023-11-13]`: write store-plan.json. No store calls. */
89
+ async function setup(argv) {
90
+ const a = args(argv);
91
+ let company = yesNo("company", a.company);
92
+ let androidNow = yesNo("android-now", a["android-now"]);
93
+ let before = yesNo("play-account-before-2023-11-13", a["play-account-before-2023-11-13"]);
94
+ if (company === undefined && process.stdin.isTTY) {
95
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
96
+ company = await ask(rl, "Do you have a registered company?", false);
97
+ if (!company) {
98
+ androidNow ??= await ask(rl, "Do you need Android live right now?", false);
99
+ before ??= await ask(rl, "Did you make a Google Play developer account before 13 Nov 2023?", false);
100
+ }
101
+ rl.close();
102
+ }
103
+ const answers = { company: company ?? false, androidNow: androidNow ?? false, playAccountBefore20231113: before ?? false };
104
+ const plan = { version: 1, answers, marks: {} };
105
+ writeFileSync(PLAN_FILE, JSON.stringify(plan, null, 2) + "\n");
106
+ console.log("Your store plan:\n");
107
+ printPlan(plan, false);
108
+ console.log(`\nWrote ${PLAN_FILE}. Run \`shipvo store status\` to see what to do next. Nothing was uploaded.`);
109
+ }
110
+ /** `shipvo store status [--mark <step> [--as waiting|done]]`: one timeline per store. */
111
+ async function status(argv) {
112
+ if (!existsSync(PLAN_FILE))
113
+ fail(`No ${PLAN_FILE} here. Run \`shipvo store setup\` first.`);
114
+ const plan = JSON.parse(readFileSync(PLAN_FILE, "utf8"));
115
+ const a = args(argv);
116
+ if (a.mark !== undefined) {
117
+ const r = routeStore(plan.answers);
118
+ const ids = [...r.play.steps, ...r.apple.steps].map((s) => s.id);
119
+ if (!ids.includes(a.mark))
120
+ fail(`Unknown step "${a.mark}". Steps: ${ids.join(", ")}.`);
121
+ const as = a.as ?? "done";
122
+ if (as !== "waiting" && as !== "done")
123
+ fail(`--as must be waiting or done, not "${as}".`);
124
+ plan.marks[a.mark] = as;
125
+ writeFileSync(PLAN_FILE, JSON.stringify(plan, null, 2) + "\n");
126
+ console.log(`Marked ${a.mark} as ${as}.\n`);
127
+ }
128
+ printPlan(plan, true);
129
+ }
130
+ /** Sets filled from app routes, with the one size we render each at and the pixel ratio. Feature graphic and icon are not app screens. */
131
+ export const RENDER_SETS = [
132
+ { set: "iphone", width: 1290, height: 2796, scale: 3 },
133
+ { set: "ipad-13", width: 2064, height: 2752, scale: 2 },
134
+ { set: "play-phone", width: 1080, height: 1920, scale: 3 },
135
+ { set: "play-tablet-7", width: 1200, height: 1920, scale: 2 },
136
+ { set: "play-tablet-10", width: 1200, height: 1920, scale: 2 },
137
+ ];
138
+ /** Fast mode: render each route at each set's size, write files, print the checker result per file. Returns the exit code. */
139
+ export async function renderScreenshots(routes, render, outDir) {
140
+ console.log("fast mode: simulator render, not the native UI");
141
+ const screens = [undefined, ...(routes ?? [])];
142
+ let failed = 0;
143
+ for (const { set, width, height, scale } of RENDER_SETS) {
144
+ const spec = SCREENSHOT_SETS[set];
145
+ const dir = join(outDir, set);
146
+ mkdirSync(dir, { recursive: true });
147
+ const used = screens.slice(0, spec.max);
148
+ if (used.length < screens.length)
149
+ console.log(`${spec.label}: the store takes ${spec.max} at most; rendering the first ${spec.max} of ${screens.length} screens.`);
150
+ const files = [];
151
+ for (const [i, route] of used.entries()) {
152
+ const slug = route === undefined ? "start" : route.replace(/^[a-z][a-z0-9+.-]*:\/*/i, "").replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").toLowerCase() || "route";
153
+ const base = `${String(i + 1).padStart(2, "0")}-${slug}`;
154
+ const bytes = await render({ width, height, scale }, route);
155
+ const r = checkScreenshot({ name: base, bytes }, set);
156
+ const file = join(dir, `${base}.${r.format === "png" ? "png" : r.format === "jpeg" ? "jpg" : "bin"}`);
157
+ writeFileSync(file, bytes);
158
+ files.push({ name: file, bytes });
159
+ if (r.ok)
160
+ console.log(`${file}: ok`);
161
+ else {
162
+ failed++;
163
+ console.log(`${file}: FAILED`);
164
+ for (const p of r.problems)
165
+ console.log(` - ${p}`);
166
+ }
167
+ }
168
+ const c = checkSet(files, set);
169
+ for (const p of c.problems.filter((p) => /file\(s\)/.test(p))) {
170
+ failed++;
171
+ console.log(`${spec.label}: ${p}`);
172
+ }
173
+ for (const n of c.notes ?? [])
174
+ console.log(`${spec.label}: note: ${n}`);
175
+ }
176
+ console.log(`\nFiles are in ${outDir}. Check the sizes against the store pages before you submit.`);
177
+ return failed ? 1 : 0;
178
+ }
179
+ /** `shipvo store screenshots [--out dir] [--sim url] [--port n] [--skip-build]`: fast mode only. */
180
+ async function screenshots(argv) {
181
+ const a = args(argv);
182
+ if (a.mode !== undefined && a.mode !== "fast")
183
+ fail(`shipvo store screenshots: only fast mode exists so far; --mode ${a.mode} is not available.`);
184
+ const cfg = loadConfig();
185
+ const out = a.out && a.out !== "true" ? a.out : "store-assets/raw";
186
+ const sim = await startSim(a, cfg);
187
+ let code = 1;
188
+ try {
189
+ const post = async (path, body) => {
190
+ const r = await fetch(sim.base.replace(/\/$/, "") + path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), signal: AbortSignal.timeout(60000) });
191
+ if (!r.ok)
192
+ throw new Error(`simulator ${path} -> ${r.status}`);
193
+ return (await r.json());
194
+ };
195
+ const session = `shots-${Date.now().toString(36)}`;
196
+ code = await renderScreenshots(cfg.routes, async (size, route) => {
197
+ const open = await post("/session", { session, app: cfg.appId, ...size });
198
+ if (open.ok === false)
199
+ throw new Error(`could not open the app: ${open.error ?? "unknown error"}`);
200
+ if (route !== undefined) {
201
+ const r = await post("/act", { session, action: "open_url", url: route });
202
+ if (!r.ok)
203
+ throw new Error(`could not open ${route}: ${r.error ?? "unknown error"}`);
204
+ }
205
+ const shot = await post("/act", { session, action: "screenshot", format: "jpeg", inline: true });
206
+ if (!shot.ok || !shot.data)
207
+ throw new Error(`screenshot failed: ${shot.error ?? "no data"}`);
208
+ return new Uint8Array(Buffer.from(shot.data, "base64"));
209
+ }, out);
210
+ }
211
+ finally {
212
+ sim.stop();
213
+ }
214
+ process.exitCode = code;
215
+ }
216
+ const PLAY_API = "https://androidpublisher.googleapis.com/androidpublisher/v3/applications";
217
+ const b64u = (b) => Buffer.from(b).toString("base64url");
218
+ /** Trade a Play service account key for an access token. The key stays in memory; it is never stored. */
219
+ async function playToken(key, f) {
220
+ const now = Math.floor(Date.now() / 1000);
221
+ const head = `${b64u(JSON.stringify({ alg: "RS256", typ: "JWT" }))}.${b64u(JSON.stringify({ iss: key.client_email, scope: "https://www.googleapis.com/auth/androidpublisher", aud: "https://oauth2.googleapis.com/token", iat: now, exp: now + 3600 }))}`;
222
+ const sig = createSign("RSA-SHA256").update(head).sign(key.private_key).toString("base64url");
223
+ const r = await f("https://oauth2.googleapis.com/token", { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: `${head}.${sig}` }) });
224
+ const j = (await r.json().catch(() => ({})));
225
+ if (!r.ok || !j.access_token)
226
+ throw new Error(`Google sign-in with the service account key failed (${r.status})`);
227
+ return j.access_token;
228
+ }
229
+ /** Add the group to a track's Google Groups through one Play edit, keeping groups already there. Returns the final list. */
230
+ export async function addTesterGroup(opts) {
231
+ const f = opts.fetch ?? fetch, track = opts.track ?? "alpha", base = `${PLAY_API}/${encodeURIComponent(opts.pkg)}/edits`;
232
+ const call = async (url, method, body) => {
233
+ const r = await f(url, { method, headers: { authorization: `Bearer ${opts.token}`, "content-type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body) });
234
+ const j = (await r.json().catch(() => ({})));
235
+ if (!r.ok)
236
+ throw new Error(`Play API ${method} ${url.replace(PLAY_API, "")} -> ${r.status}${j.error?.message ? `: ${j.error.message}` : ""}`);
237
+ return j;
238
+ };
239
+ const edit = await call(base, "POST", {});
240
+ const cur = await call(`${base}/${edit.id}/testers/${track}`, "GET");
241
+ const groups = Array.isArray(cur.googleGroups) ? cur.googleGroups : [];
242
+ if (groups.includes(opts.group)) {
243
+ await call(`${base}/${edit.id}`, "DELETE").catch(() => { });
244
+ return { groups, added: false };
245
+ }
246
+ const next = [...groups, opts.group];
247
+ await call(`${base}/${edit.id}/testers/${track}`, "PUT", { googleGroups: next });
248
+ await call(`${base}/${edit.id}:commit`, "POST");
249
+ return { groups: next, added: true };
250
+ }
251
+ export function testersGuide(pkg, track = "alpha") {
252
+ return {
253
+ package: pkg, track,
254
+ group: SHIPVO_TESTERS_GROUP, joinUrl: SHIPVO_TESTERS_JOIN_URL,
255
+ optInUrl: `https://play.google.com/apps/testing/${pkg}`,
256
+ firstUploadByHand: "Play requires the first upload of a new app to be made by hand in Play Console. This command cannot do that.",
257
+ steps: [
258
+ "In Play Console, open Testing > Closed testing and create a track (or use the default one).",
259
+ "Upload your first build by hand. Play does not allow the API to do the first upload.",
260
+ `Add testers as Google Groups: ${SHIPVO_TESTERS_GROUP}, plus a group of your own people. Or run \`shipvo store testers --play-key <file>\` to add it through the API.`,
261
+ `Share your opt-in link: https://play.google.com/apps/testing/${pkg}`,
262
+ `Join Shipvo Testers yourself (${SHIPVO_TESTERS_JOIN_URL}) and test other founders' apps. The community works because everyone tests.`,
263
+ "Keep at least 12 testers opted in for 14 days in a row. The clock starts when 12 have opted in; if the count drops below 12 it resets.",
264
+ ],
265
+ };
266
+ }
267
+ /** `shipvo store testers [--package id] [--track alpha] [--play-key file] [--json]`: print the steps. Calls Play only with --play-key. */
268
+ async function testers(argv) {
269
+ const a = args(argv);
270
+ const pkg = a.package && a.package !== "true" ? a.package : loadConfig().appId;
271
+ const track = a.track && a.track !== "true" ? a.track : "alpha";
272
+ const guide = testersGuide(pkg, track);
273
+ let api;
274
+ if (a["play-key"] !== undefined) {
275
+ if (a["play-key"] === "true")
276
+ fail("shipvo store testers: --play-key needs a file path.");
277
+ try {
278
+ const key = JSON.parse(readFileSync(a["play-key"], "utf8"));
279
+ api = await addTesterGroup({ pkg, track, group: SHIPVO_TESTERS_GROUP, token: await playToken(key, fetch) });
280
+ }
281
+ catch (e) {
282
+ api = { error: e.message };
283
+ process.exitCode = 1;
284
+ }
285
+ }
286
+ if (a.json !== undefined && a.json !== "false") {
287
+ console.log(JSON.stringify({ ...guide, ...(api ? { api } : {}) }, null, 2));
288
+ return;
289
+ }
290
+ console.log(`Get your 12 testers for ${pkg} (closed track "${track}"):\n`);
291
+ guide.steps.forEach((s, i) => console.log(` ${i + 1}. ${s}`));
292
+ console.log(`\n${guide.firstUploadByHand}`);
293
+ if (api)
294
+ console.log("error" in api ? `\nCould not update the track: ${api.error}` : api.added ? `\nAdded ${SHIPVO_TESTERS_GROUP} to the "${track}" track. Groups now: ${api.groups.join(", ")}.` : `\n${SHIPVO_TESTERS_GROUP} was already on the "${track}" track. Nothing changed.`);
295
+ else
296
+ console.log("\nNo Play call was made. Pass --play-key <service-account.json> to add the group through the API; the key is read once and never stored.");
297
+ }
@@ -0,0 +1,122 @@
1
+ # Shipvo build template for Capacitor apps. Written by `shipvo ci init --framework capacitor`.
2
+ # Builds a signed Android bundle and a signed iOS app in your own GitHub Actions.
3
+ # Add the secrets listed in docs/capacitor.md (Settings > Secrets and variables > Actions).
4
+ name: Shipvo build
5
+
6
+ permissions:
7
+ contents: read
8
+
9
+ on:
10
+ workflow_dispatch:
11
+ push:
12
+ tags: ["v*"]
13
+
14
+ jobs:
15
+ android:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: actions/setup-node@v4
20
+ with:
21
+ node-version: 22
22
+ cache: npm
23
+ - uses: actions/setup-java@v4
24
+ with:
25
+ distribution: temurin
26
+ java-version: 21
27
+ - run: npm ci
28
+ - run: npm run build
29
+ - run: npx cap sync android
30
+ - name: Decode the upload keystore
31
+ env:
32
+ ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
33
+ run: echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/upload.keystore"
34
+ - name: Build the signed bundle
35
+ working-directory: android
36
+ env:
37
+ ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
38
+ ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
39
+ ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
40
+ run: >
41
+ ./gradlew bundleRelease
42
+ -Pandroid.injected.signing.store.file="$RUNNER_TEMP/upload.keystore"
43
+ -Pandroid.injected.signing.store.password="$ANDROID_KEYSTORE_PASSWORD"
44
+ -Pandroid.injected.signing.key.alias="$ANDROID_KEY_ALIAS"
45
+ -Pandroid.injected.signing.key.password="$ANDROID_KEY_PASSWORD"
46
+ - uses: actions/upload-artifact@v4
47
+ with:
48
+ name: android-release
49
+ path: android/app/build/outputs/bundle/release/*.aab
50
+ if-no-files-found: error
51
+
52
+ ios:
53
+ runs-on: macos-latest
54
+ steps:
55
+ - uses: actions/checkout@v4
56
+ - uses: actions/setup-node@v4
57
+ with:
58
+ node-version: 22
59
+ cache: npm
60
+ - run: npm ci
61
+ - run: npm run build
62
+ - run: npx cap sync ios
63
+ - name: Install the signing certificate and profile
64
+ env:
65
+ IOS_CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }}
66
+ IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
67
+ IOS_PROFILE_BASE64: ${{ secrets.IOS_PROFILE_BASE64 }}
68
+ KEYCHAIN_PASSWORD: ${{ secrets.IOS_KEYCHAIN_PASSWORD }}
69
+ run: |
70
+ KEYCHAIN="$RUNNER_TEMP/build.keychain-db"
71
+ echo "$IOS_CERTIFICATE_BASE64" | base64 --decode > "$RUNNER_TEMP/cert.p12"
72
+ security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
73
+ security set-keychain-settings -lut 21600 "$KEYCHAIN"
74
+ security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
75
+ security import "$RUNNER_TEMP/cert.p12" -P "$IOS_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN"
76
+ security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
77
+ security list-keychain -d user -s "$KEYCHAIN" "$HOME/Library/Keychains/login.keychain-db"
78
+ mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
79
+ echo "$IOS_PROFILE_BASE64" | base64 --decode > "$HOME/Library/MobileDevice/Provisioning Profiles/build.mobileprovision"
80
+ - name: Archive
81
+ env:
82
+ IOS_SCHEME: ${{ vars.IOS_SCHEME || 'App' }}
83
+ IOS_TEAM_ID: ${{ secrets.IOS_TEAM_ID }}
84
+ IOS_PROFILE_NAME: ${{ secrets.IOS_PROFILE_NAME }}
85
+ run: >
86
+ xcodebuild archive
87
+ -workspace ios/App/App.xcworkspace
88
+ -scheme "$IOS_SCHEME"
89
+ -configuration Release
90
+ -archivePath "$RUNNER_TEMP/App.xcarchive"
91
+ DEVELOPMENT_TEAM="$IOS_TEAM_ID"
92
+ CODE_SIGN_STYLE=Manual
93
+ PROVISIONING_PROFILE_SPECIFIER="$IOS_PROFILE_NAME"
94
+ CODE_SIGN_IDENTITY="Apple Distribution"
95
+ - name: Export the .ipa
96
+ env:
97
+ IOS_TEAM_ID: ${{ secrets.IOS_TEAM_ID }}
98
+ IOS_PROFILE_NAME: ${{ secrets.IOS_PROFILE_NAME }}
99
+ IOS_BUNDLE_ID: ${{ secrets.IOS_BUNDLE_ID }}
100
+ run: |
101
+ cat > "$RUNNER_TEMP/ExportOptions.plist" <<PLIST
102
+ <?xml version="1.0" encoding="UTF-8"?>
103
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
104
+ <plist version="1.0"><dict>
105
+ <key>method</key><string>app-store-connect</string>
106
+ <key>teamID</key><string>$IOS_TEAM_ID</string>
107
+ <key>signingStyle</key><string>manual</string>
108
+ <key>provisioningProfiles</key><dict><key>$IOS_BUNDLE_ID</key><string>$IOS_PROFILE_NAME</string></dict>
109
+ </dict></plist>
110
+ PLIST
111
+ xcodebuild -exportArchive -archivePath "$RUNNER_TEMP/App.xcarchive" -exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist" -exportPath "$RUNNER_TEMP/export"
112
+ mkdir -p dist-ios && cp "$RUNNER_TEMP"/export/*.ipa dist-ios/
113
+ - uses: actions/upload-artifact@v4
114
+ with:
115
+ name: ios-release
116
+ path: dist-ios/*.ipa
117
+ if-no-files-found: error
118
+ - name: Remove the keychain and certificate
119
+ if: always()
120
+ run: |
121
+ security delete-keychain "$RUNNER_TEMP/build.keychain-db" || true
122
+ rm -f "$RUNNER_TEMP/cert.p12"
@@ -0,0 +1,18 @@
1
+ name: Shipvo check
2
+
3
+ on:
4
+ pull_request:
5
+
6
+ permissions:
7
+ contents: read
8
+ pull-requests: write
9
+
10
+ jobs:
11
+ check:
12
+ runs-on: ubuntu-latest
13
+ timeout-minutes: 30
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ with:
17
+ fetch-depth: 0
18
+ - uses: shipvo/shipvo/actions/check@main
package/dist/test.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ type Node = {
2
+ role: string;
3
+ name: string;
4
+ text: string;
5
+ ref: string;
6
+ };
7
+ type StepLike = {
8
+ kind: string;
9
+ target?: string;
10
+ text?: string;
11
+ enter?: boolean;
12
+ line: number;
13
+ };
14
+ type PhoneLike = {
15
+ screen: string;
16
+ };
17
+ export type TestKit = {
18
+ openApp(): Promise<PhoneLike>;
19
+ parseSteps(text: string): StepLike[];
20
+ runSteps(phone: PhoneLike, steps: StepLike[]): Promise<void>;
21
+ redact?(text: string): string;
22
+ proposeRepair(step: StepLike, tree: string): {
23
+ kind: "repaired";
24
+ step: StepLike;
25
+ reason: string;
26
+ } | {
27
+ kind: "unresolved";
28
+ candidates: Node[];
29
+ };
30
+ };
31
+ /** The same step as a plain-language line (only the target of a tap or type step is ever rewritten). */
32
+ export declare function stepLine(s: StepLike): string;
33
+ /** A unified diff that replaces single lines (line numbers are 1-based, old and new have the same count). Empty when nothing changes. */
34
+ export declare function unifiedDiff(file: string, text: string, edits: Map<number, string>): string;
35
+ /** Apply line edits, keeping indentation. Every other line, including comments and blank lines, is untouched. */
36
+ export declare function applyEdits(text: string, edits: Map<number, string>): string;
37
+ export type FlowReport = {
38
+ name: string;
39
+ status: "passed" | "failed";
40
+ steps: StepLike[];
41
+ failedStep: StepLike | null;
42
+ message: string | null;
43
+ };
44
+ /** Runs the suite. Returns the exit code: 0 when every step passed (or every failure was repaired and written), 1 otherwise. */
45
+ export declare function testCmd(argv: string[], kit?: TestKit, rawLog?: (s: string) => void, fetchFn?: typeof fetch): Promise<number>;
46
+ export {};