@shiplane/eas 0.0.0-stage โ†’ 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arnaud Zheng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,93 @@
1
- # Temporary Holding Version
1
+ # ๐Ÿ”Œ @shiplane/eas
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Ships an Expo app with EAS: fills [shiplane](../shiplane/README.md)'s **mobile distribution** port.
4
+ Each release is a store build or an over-the-air update, picked from the native fingerprint.
5
+
6
+ ```mermaid
7
+ flowchart LR
8
+ CFG["shiplane.config.ts"] --> MOD["mobileApp"]
9
+ MOD --> EAS["easMobileDistribution()"]
10
+ EAS --> Q["Did the native layer change?"]
11
+ Q -->|"yes"| BUILD["eas build, submitted to the stores"]
12
+ Q -->|"no"| UPDATE["your update script, over the air"]
13
+ ```
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ pnpm add -D shiplane @shiplane/eas
19
+ ```
20
+
21
+ ## Configure
22
+
23
+ Give the adapter to `mobileApp`. From
24
+ [`examples/shiplane.config.ts`](./examples/shiplane.config.ts):
25
+
26
+ ```ts
27
+ import { changesetsVersioning } from "@shiplane/changesets";
28
+ import { easMobileDistribution } from "@shiplane/eas";
29
+ import { gitSourceControl } from "@shiplane/git";
30
+ import { gitHubActionsCiPipeline, gitHubCodeHost } from "@shiplane/github";
31
+ import { defineRelease, mobileApp, releaseFlow } from "shiplane";
32
+
33
+ export default defineRelease({
34
+ flow: releaseFlow({
35
+ sourceControl: gitSourceControl(),
36
+ codeHost: gitHubCodeHost(),
37
+ ciPipeline: gitHubActionsCiPipeline(),
38
+ versioning: changesetsVersioning(),
39
+ }),
40
+ modules: [mobileApp({ mobileDistribution: easMobileDistribution() })],
41
+ });
42
+ ```
43
+
44
+ ## Unit file keys
45
+
46
+ In the app's `shiplane.unit.yml`, next to `module: mobile-app`.
47
+
48
+ | Key | Default | Means |
49
+ | ------------- | ------------------ | ------------------------------------------------------------ |
50
+ | `update` | required | The package script that publishes an update, per environment |
51
+ | `versionFile` | `app-version.json` | Each environment's binary version |
52
+
53
+ Source: `EasOptions` in `src/eas-mobile-distribution.adapter.ts`.
54
+
55
+ ## Example
56
+
57
+ From [`examples/apps/mobile/shiplane.unit.yml`](./examples/apps/mobile/shiplane.unit.yml):
58
+
59
+ ```yaml
60
+ module: mobile-app
61
+ update:
62
+ preview: eas:update:preview
63
+ production: eas:update:production
64
+ ```
65
+
66
+ `pnpm shiplane plan` in that repository prints both ways a ship to preview could go:
67
+
68
+ ```text
69
+ Plan ยท preview ยท nothing runs
70
+ Order 1 mobile ยท Latest
71
+
72
+ mobile@1.0.0 โ†’ EAS preview, iOS and Android (not tagged yet: release first)
73
+ this machine
74
+ EXPO_ATLAS=false APP_VARIANT=preview pnpm exec eas build --profile preview --platform all --auto-submit --no-wait
75
+ this machine
76
+ APP_VARIANT=preview pnpm run eas:update:preview
77
+ GitHub Actions
78
+ gh workflow run ship.yml --ref mobile@1.0.0 -f units=mobile -f environment=preview
79
+ ```
80
+
81
+ At release time EAS decides: a build when the fingerprint moved, an update when it did not. Builds
82
+ run from this machine and return at once (`--no-wait`). An update script with `--rollout-percentage`
83
+ starts a staged rollout that `shiplane promote` widens. `init` offers this adapter for a folder with
84
+ an `eas.json`.
85
+
86
+ ## Tools and sign-ins
87
+
88
+ | Tool | Checked with | Fixed with | In CI |
89
+ | ----- | ---------------------------------- | --------------------------------- | ------------ |
90
+ | `eas` | `pnpm exec eas whoami`, in the app | `pnpm exec eas login`, in the app | `EXPO_TOKEN` |
91
+
92
+ Source: `requirements` in `src/eas-mobile-distribution.adapter.ts`. `pnpm shiplane doctor` runs each
93
+ check and offers the fix.
@@ -0,0 +1,12 @@
1
+ import { DeliveryAdapter, MobileDistributionPort } from "shiplane/plugin";
2
+
3
+ //#region src/eas-mobile-distribution.adapter.d.ts
4
+ /**
5
+ * A mobile app on EAS. The native fingerprint decides between a new binary and an over-the-air
6
+ * update; production ships what preview validated, moving its version by PR first when it must.
7
+ */
8
+ type EasMobileDistributionAdapter = DeliveryAdapter<MobileDistributionPort>;
9
+ declare const easMobileDistribution: () => EasMobileDistributionAdapter;
10
+ //#endregion
11
+ export { type EasMobileDistributionAdapter, easMobileDistribution };
12
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,937 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { compareVersions, defineMobileDistribution, nextPatch, perEnv, progressBar, releaseSource, tagName, z } from "shiplane/plugin";
4
+ import { styleText } from "node:util";
5
+ //#region src/eas/policy.ts
6
+ /** What this commit needs on one platform, given the builds on its channel. */
7
+ const platformVerdict = ({ platform, local, shipped, running }) => {
8
+ if (running?.appVersion === local.version) return {
9
+ platform,
10
+ action: "wait",
11
+ why: "build-running",
12
+ running
13
+ };
14
+ const newestVersion = running?.appVersion ?? shipped?.appVersion;
15
+ if (shipped && newestVersion && compareVersions(local.version, newestVersion) < 0) return {
16
+ platform,
17
+ action: "pull",
18
+ why: "version-behind",
19
+ shipped,
20
+ newerVersion: newestVersion
21
+ };
22
+ if (!shipped) return {
23
+ platform,
24
+ action: "build",
25
+ why: "no-build",
26
+ shipped
27
+ };
28
+ if (shipped.appVersion && shipped.appVersion !== local.version) return {
29
+ platform,
30
+ action: "build",
31
+ why: "version-changed",
32
+ shipped
33
+ };
34
+ if (!shipped.fingerprint) return {
35
+ platform,
36
+ action: "build",
37
+ why: "no-fingerprint",
38
+ shipped
39
+ };
40
+ if (shipped.fingerprint !== local.fingerprint) return {
41
+ platform,
42
+ action: "build",
43
+ why: "native-changed",
44
+ shipped
45
+ };
46
+ if (shipped.gitCommit === local.commit) return {
47
+ platform,
48
+ action: "submit",
49
+ shipped
50
+ };
51
+ return {
52
+ platform,
53
+ action: "update",
54
+ shipped
55
+ };
56
+ };
57
+ const PRECEDENCE = [
58
+ "pull",
59
+ "build",
60
+ "wait",
61
+ "submit",
62
+ "update"
63
+ ];
64
+ /** The next step for the whole release, with the release CLI commands that take it. */
65
+ const releasePlan = (channel, verdicts, unit) => {
66
+ const action = PRECEDENCE.find((candidate) => verdicts.some((verdict) => verdict.action === candidate)) ?? "update";
67
+ const flag = channel === "preview" ? "" : ` --channel ${channel}`;
68
+ return {
69
+ action,
70
+ commands: {
71
+ build: [`pnpm shiplane ship ${unit}${flag}`],
72
+ pull: [
73
+ "git fetch origin",
74
+ "git merge origin/main",
75
+ `pnpm shiplane${flag}`
76
+ ],
77
+ wait: [`pnpm shiplane status${flag}`],
78
+ submit: [`pnpm shiplane status${flag}`],
79
+ update: [`pnpm shiplane ship ${unit}${flag}`]
80
+ }[action]
81
+ };
82
+ };
83
+ /**
84
+ * A build of a version some binary already carries would give two native layers one version: each
85
+ * binary keeps its own. The release pull request, or the promotion one, moves it first.
86
+ */
87
+ const needsNewVersion = (states) => states.some(({ local, shipped }) => shipped?.appVersion === local.version);
88
+ /**
89
+ * What the release does for mobile, from preview's verdict. A new native layer gets a new
90
+ * version, so each binary's version names one native layer.
91
+ */
92
+ const mobileRelease = (action, expo, currentVersion) => {
93
+ if (action === "wait") return {
94
+ ships: "blocked",
95
+ why: "build-running"
96
+ };
97
+ if (action === "pull") return {
98
+ ships: "blocked",
99
+ why: "behind"
100
+ };
101
+ if (action === "build") return expo ? {
102
+ ships: "build",
103
+ version: expo.to,
104
+ nativeChangeset: false
105
+ } : {
106
+ ships: "build",
107
+ version: nextPatch(currentVersion),
108
+ nativeChangeset: true
109
+ };
110
+ return expo ? {
111
+ ships: "update",
112
+ version: expo.to
113
+ } : { ships: "nothing" };
114
+ };
115
+ /** The changeset the release writes itself: a build gets a version even with no expo changeset. */
116
+ const releaseChangesets = (mobile, changes, app) => mobile.ships === "build" && mobile.nativeChangeset ? [{
117
+ id: `native-${mobile.version}`,
118
+ releases: [{
119
+ name: app,
120
+ type: "patch"
121
+ }],
122
+ summary: changes.length > 0 ? `Native code changed: ${changes.map((change) => change.replace(/^[+~-] /, "")).join(", ")}.` : "A new build of the app, with the same native code."
123
+ }] : [];
124
+ const BUILD_LABEL = "New build, iOS and Android";
125
+ const UPDATE_LABEL = "Over-the-air update";
126
+ /**
127
+ * What the person may pick once detection spoke, the suggestion first: a build is always possible,
128
+ * an update only on the native layer the installed binaries carry.
129
+ */
130
+ const mobileChoice = (action, why, app) => {
131
+ if (action === "wait" || action === "pull") return void 0;
132
+ const question = `What should ${app} ship to preview?`;
133
+ if (action === "build") return {
134
+ question,
135
+ suggested: "build",
136
+ options: [{
137
+ value: "build",
138
+ label: `${BUILD_LABEL} (suggested)`,
139
+ hint: why
140
+ }, {
141
+ value: "update",
142
+ label: UPDATE_LABEL,
143
+ hint: "not safe: installed binaries lack this native layer",
144
+ disabled: true
145
+ }]
146
+ };
147
+ return {
148
+ question,
149
+ suggested: "update",
150
+ options: [{
151
+ value: "update",
152
+ label: `${UPDATE_LABEL} (suggested)`,
153
+ hint: why
154
+ }, {
155
+ value: "build",
156
+ label: BUILD_LABEL,
157
+ hint: "not needed: a new binary anyway, under its own version"
158
+ }]
159
+ };
160
+ };
161
+ /**
162
+ * What mobile still needs for the expo version a release tagged. A binary or an update ships its
163
+ * commit: once the app's code moved past the release, it would carry code no release named.
164
+ */
165
+ const mobileShip = (action, state) => {
166
+ if (!state.tagged) return "nothing";
167
+ if (action === "wait") return "wait";
168
+ if (action === "pull") return "behind";
169
+ if (action === "submit") return "shipped";
170
+ if (action === "update" && state.codeShipped) return "shipped";
171
+ if (state.codeSinceTag) return "moved-past";
172
+ return action;
173
+ };
174
+ /**
175
+ * What production still needs to run the expo version preview validated. A new native layer needs
176
+ * production's version moved first (the promotion PR).
177
+ */
178
+ const productionMobile = (action, state) => {
179
+ if (!state.tagged) return "nothing";
180
+ if (action === "wait") return "wait";
181
+ if (action === "pull") return "behind";
182
+ if (action !== "build" && state.codeShipped) return state.partialRollout ? "finish-rollout" : "shipped";
183
+ if (!state.sinceTagOnlyVersionFiles) return "moved-past";
184
+ if (action === "build") return state.productionVersion === state.expoVersion ? "build" : "promote";
185
+ return "update";
186
+ };
187
+ /** The shares of people a production rollout widens through, in order. */
188
+ const ROLLOUT_STEPS = [
189
+ 10,
190
+ 25,
191
+ 50,
192
+ 100
193
+ ];
194
+ const counted = (total, one, many) => `${total} ${total === 1 ? one : many}`;
195
+ /** How long an update has been out, as a person says it: hours under two days, then days. */
196
+ const timeOut = (hours) => hours < 48 ? counted(Math.max(1, Math.floor(hours)), "hour", "hours") : counted(Math.floor(hours / 24), "day", "days");
197
+ /**
198
+ * The next move for an update reaching part of production: hold while it crashed, nobody ran it or
199
+ * it is under a day old; otherwise the next step up.
200
+ */
201
+ const rolloutAdvice = ({ percentage, launches, crashes, hoursOut }) => {
202
+ if (crashes > 0) return {
203
+ suggested: "stay",
204
+ why: `${counted(crashes, "crash", "crashes")} since it went out: look before widening, or roll back`
205
+ };
206
+ if (launches === 0) return {
207
+ suggested: "stay",
208
+ why: "nobody has launched it yet: wait for launches"
209
+ };
210
+ if (hoursOut < 24) return {
211
+ suggested: "stay",
212
+ why: `out for ${timeOut(hoursOut)}: give it a day`
213
+ };
214
+ return {
215
+ suggested: ROLLOUT_STEPS.find((step) => step > percentage) ?? 100,
216
+ why: `healthy for ${timeOut(hoursOut)}: widen it`
217
+ };
218
+ };
219
+ //#endregion
220
+ //#region src/eas/analyse.ts
221
+ const PLATFORMS = ["ios", "android"];
222
+ const PLATFORM_NAME = {
223
+ ios: "iOS",
224
+ android: "Android"
225
+ };
226
+ /** Each platform's last store build on the channel against this commit's native fingerprint. */
227
+ const analyse = async (client, source, environment, version, unit) => {
228
+ const position = await source.position();
229
+ const checked = await Promise.all(PLATFORMS.map(async (platform) => {
230
+ const [fingerprint, builds] = await Promise.all([client.localFingerprint(platform, environment), client.channelBuilds(platform, environment)]);
231
+ const state = {
232
+ platform,
233
+ local: {
234
+ fingerprint,
235
+ version,
236
+ commit: position.head
237
+ },
238
+ ...builds
239
+ };
240
+ const verdict = platformVerdict(state);
241
+ return {
242
+ state,
243
+ verdict,
244
+ changes: verdict.action === "build" && verdict.why === "native-changed" && verdict.shipped ? await client.nativeChanges(verdict.shipped.id, environment) : []
245
+ };
246
+ }));
247
+ return {
248
+ channel: environment,
249
+ version,
250
+ commit: position.head,
251
+ git: position,
252
+ checked,
253
+ plan: releasePlan(environment, checked.map(({ verdict }) => verdict), unit)
254
+ };
255
+ };
256
+ const LOOK = {
257
+ update: {
258
+ mark: "โœ“",
259
+ style: "green"
260
+ },
261
+ submit: {
262
+ mark: "โ†‘",
263
+ style: "green"
264
+ },
265
+ wait: {
266
+ mark: "โ€ฆ",
267
+ style: "yellow"
268
+ },
269
+ pull: {
270
+ mark: "โ†“",
271
+ style: "yellow"
272
+ },
273
+ build: {
274
+ mark: "โœ—",
275
+ style: "red"
276
+ }
277
+ };
278
+ /** Why a platform needs what it needs, as a person reads it. */
279
+ const why = (verdict) => {
280
+ switch (verdict.action) {
281
+ case "update": return "same native layer and version as the last build";
282
+ case "submit": return "the last build was made from this commit";
283
+ case "wait": return `build ${verdict.running.buildNumber ?? "?"} for this version is still running`;
284
+ case "pull": return `this branch misses the ${verdict.newerVersion} release of a build on this channel`;
285
+ case "build": return {
286
+ "no-build": "no store build on this channel yet",
287
+ "no-fingerprint": "the last build has no fingerprint, nothing proves an update fits",
288
+ "native-changed": "native code changed since the last build",
289
+ "version-changed": `no binary carries this version yet: the last finished build is ${verdict.shipped?.appVersion ?? "older"}`
290
+ }[verdict.why];
291
+ }
292
+ };
293
+ /** Why the deciding platform needs what the plan says, or nothing when none decided. */
294
+ const planWhy = ({ checked, plan }) => {
295
+ const shown = checked.find(({ verdict }) => verdict.action === plan.action);
296
+ return shown ? why(shown.verdict) : "";
297
+ };
298
+ const MAX_CHANGES = 6;
299
+ /** The verdict line a summary shows for the platform that decides, with what changed under it. */
300
+ const verdictLines = ({ checked, plan }) => {
301
+ const shown = checked.find(({ verdict }) => verdict.action === plan.action);
302
+ if (!shown) return [];
303
+ const look = LOOK[shown.verdict.action];
304
+ return [`${styleText(look.style, `${look.mark} ${shown.verdict.action}`)} ${styleText("dim", why(shown.verdict))}`, ...shown.changes.slice(0, MAX_CHANGES).map((change) => styleText("dim", ` ${change}`))];
305
+ };
306
+ /** The analysis as `status --json` prints it, the same shape the old release check printed. */
307
+ const analysisJson = ({ channel, version, commit, plan, checked }) => ({
308
+ channel,
309
+ version,
310
+ commit,
311
+ ...plan,
312
+ platforms: checked.map(({ verdict, changes }) => ({
313
+ ...verdict,
314
+ changes
315
+ }))
316
+ });
317
+ const count = (total, one, many) => `${total} ${total === 1 ? one : many}`;
318
+ /** An update's health from its launches: unwell once it crashed, or while nobody has run it. */
319
+ const updateHealth = (launches) => {
320
+ const summary = launches.map(({ platform, installs, failedInstalls }) => `${PLATFORM_NAME[platform]} ${count(installs, "launch", "launches")}, ${count(failedInstalls, "crash", "crashes")}`).join(" ยท ");
321
+ const crashes = launches.reduce((sum, { failedInstalls }) => sum + failedInstalls, 0);
322
+ if (crashes > 0) return {
323
+ ok: false,
324
+ summary,
325
+ why: `${count(crashes, "crash", "crashes")} on preview`
326
+ };
327
+ if (launches.every(({ installs }) => installs === 0)) return {
328
+ ok: false,
329
+ summary,
330
+ why: "nobody has launched it on preview yet"
331
+ };
332
+ return {
333
+ ok: true,
334
+ summary
335
+ };
336
+ };
337
+ /** Where a rollout stands, as the lines above the question: whose, how far, how long, how it does. */
338
+ const rolloutLines = ({ tag, environment, percentage, hoursOut, health }) => [
339
+ `${styleText("bold", tag)} ${styleText("dim", `on ${environment}`)}`,
340
+ `${styleText(health.ok ? "green" : "yellow", progressBar(percentage, 100, 20))} ${percentage}% of people ยท out for ${timeOut(hoursOut)}`,
341
+ styleText("dim", health.summary)
342
+ ];
343
+ //#endregion
344
+ //#region src/eas/client.ts
345
+ const parseEasJson = (output) => {
346
+ const start = output.search(/[[{]/);
347
+ const end = Math.max(output.lastIndexOf("]"), output.lastIndexOf("}"));
348
+ return JSON.parse(output.slice(start, end + 1));
349
+ };
350
+ const SourceSchema = z.object({
351
+ type: z.string(),
352
+ filePath: z.string().optional(),
353
+ id: z.string().optional(),
354
+ hash: z.string().nullish()
355
+ });
356
+ const FingerprintSchema = z.object({
357
+ hash: z.string(),
358
+ sources: z.array(SourceSchema)
359
+ });
360
+ const EasBuildSchema = z.object({
361
+ id: z.string(),
362
+ status: z.string(),
363
+ buildProfile: z.string().nullish(),
364
+ isForIosSimulator: z.boolean().nullish(),
365
+ appVersion: z.string().nullish(),
366
+ appBuildVersion: z.string().nullish(),
367
+ gitCommitHash: z.string().nullish(),
368
+ createdAt: z.string(),
369
+ fingerprint: z.object({ hash: z.string() }).nullish()
370
+ });
371
+ const UpdateListSchema = z.object({ currentPage: z.array(z.object({
372
+ group: z.string(),
373
+ rolloutPercentage: z.number().optional()
374
+ })) });
375
+ const UpdateViewSchema = z.array(z.object({
376
+ gitCommitHash: z.string().nullish(),
377
+ createdAt: z.string().nullish()
378
+ }));
379
+ const UpdateInsightsSchema = z.object({ platforms: z.array(z.object({
380
+ platform: z.enum(["ios", "android"]),
381
+ totals: z.object({
382
+ installs: z.number(),
383
+ failedInstalls: z.number()
384
+ })
385
+ })) });
386
+ const RUNNING = new Set([
387
+ "NEW",
388
+ "IN_QUEUE",
389
+ "IN_PROGRESS"
390
+ ]);
391
+ const WITHOUT_NON_INTERACTIVE_FLAG = new Set(["update:view"]);
392
+ const failureReason = (stderr) => {
393
+ const lines = stderr.split("\n").map((line) => line.trim()).filter(Boolean);
394
+ return lines.find((line) => line.startsWith("Error:")) ?? lines.at(-1) ?? "";
395
+ };
396
+ /** The eas CLI, run in the app's folder with its environment as APP_VARIANT. */
397
+ const easClient = (shell, cwd) => {
398
+ const eas = async (args, environment) => {
399
+ const command = args[0] ?? "";
400
+ const flags = WITHOUT_NON_INTERACTIVE_FLAG.has(command) ? ["--json"] : ["--json", "--non-interactive"];
401
+ const { status, stdout, stderr } = await shell.capture("pnpm", [
402
+ "exec",
403
+ "eas",
404
+ ...args,
405
+ ...flags
406
+ ], {
407
+ cwd,
408
+ env: { APP_VARIANT: environment }
409
+ });
410
+ if (status !== 0) throw new Error(`eas ${command} exited with ${status}: ${failureReason(stderr)}`);
411
+ return parseEasJson(stdout);
412
+ };
413
+ return {
414
+ localFingerprint: async (platform, environment) => FingerprintSchema.parse(await eas([
415
+ "fingerprint:generate",
416
+ "--platform",
417
+ platform
418
+ ], environment)).hash,
419
+ channelBuilds: async (platform, environment) => {
420
+ const builds = z.array(EasBuildSchema).parse(await eas([
421
+ "build:list",
422
+ "--platform",
423
+ platform,
424
+ "--limit",
425
+ "30"
426
+ ], environment)).filter((build) => build.buildProfile === environment && !build.isForIosSimulator);
427
+ const finished = builds.find((build) => build.status === "FINISHED");
428
+ const running = builds.find((build) => RUNNING.has(build.status));
429
+ return {
430
+ shipped: finished ? {
431
+ id: finished.id,
432
+ appVersion: finished.appVersion ?? null,
433
+ buildNumber: finished.appBuildVersion ?? null,
434
+ fingerprint: finished.fingerprint?.hash ?? null,
435
+ gitCommit: finished.gitCommitHash ?? null,
436
+ createdAt: finished.createdAt
437
+ } : null,
438
+ running: running ? {
439
+ id: running.id,
440
+ appVersion: running.appVersion ?? null,
441
+ buildNumber: running.appBuildVersion ?? null
442
+ } : null
443
+ };
444
+ },
445
+ nativeChanges: async (buildId, environment) => {
446
+ const { fingerprint1, fingerprint2 } = z.object({
447
+ fingerprint1: FingerprintSchema,
448
+ fingerprint2: FingerprintSchema
449
+ }).parse(await eas([
450
+ "fingerprint:compare",
451
+ "--build-id",
452
+ buildId
453
+ ], environment));
454
+ const key = (source) => `${source.type}:${source.filePath ?? source.id ?? ""}`;
455
+ const label = (source) => (source.filePath ?? source.id ?? source.type).replace(/^(\.\.\/)+/, "");
456
+ const before = new Map(fingerprint1.sources.map((source) => [key(source), source]));
457
+ const after = new Map(fingerprint2.sources.map((source) => [key(source), source]));
458
+ return [...[...after].flatMap(([id, source]) => {
459
+ const old = before.get(id);
460
+ if (!old) return [`+ ${label(source)}`];
461
+ return old.hash === source.hash ? [] : [`~ ${label(source)}`];
462
+ }), ...[...before].flatMap(([id, source]) => after.has(id) ? [] : [`- ${label(source)}`])];
463
+ },
464
+ /** The newest update group on the channel, with the share of people it reaches. */
465
+ lastUpdate: async (environment) => {
466
+ const { currentPage } = UpdateListSchema.parse(await eas([
467
+ "update:list",
468
+ "--branch",
469
+ environment,
470
+ "--limit",
471
+ "1"
472
+ ], environment));
473
+ const [last] = currentPage;
474
+ return last ? {
475
+ group: last.group,
476
+ rolloutPercentage: last.rolloutPercentage ?? 100
477
+ } : null;
478
+ },
479
+ /** Each platform's launches of an update group over the last 7 days, and the failed ones. */
480
+ updateLaunches: async (group, environment) => UpdateInsightsSchema.parse(await eas(["update:insights", group], environment)).platforms.map(({ platform, totals }) => ({
481
+ platform,
482
+ ...totals
483
+ })),
484
+ /** The commit an update group was made from, and when it was published. */
485
+ updateOrigin: async (group, environment) => {
486
+ const [first] = UpdateViewSchema.parse(await eas(["update:view", group], environment));
487
+ return {
488
+ commit: first?.gitCommitHash ?? null,
489
+ createdAt: first?.createdAt ?? null
490
+ };
491
+ }
492
+ };
493
+ };
494
+ //#endregion
495
+ //#region src/eas-mobile-distribution.adapter.ts
496
+ const EasOptions = z.object({
497
+ versionFile: z.string().default("app-version.json").describe("Each environment's binary version"),
498
+ update: perEnv(z.string()).describe("The package script that publishes an update, per environment")
499
+ });
500
+ const readJson = (file) => z.record(z.string(), z.unknown()).parse(JSON.parse(fs.readFileSync(file, "utf8")));
501
+ const writeJson = (file, value) => fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
502
+ const VersionsSchema = z.record(z.string(), z.object({ version: z.string() }).loose());
503
+ /** The binary version an environment carries. */
504
+ const versionFileOf = (root, unit, options) => path.join(root, unit.dir, options.versionFile);
505
+ const binaryVersion = (root, unit, options, environment) => VersionsSchema.parse(readJson(versionFileOf(root, unit, options)))[environment]?.version ?? unit.version;
506
+ const setBinaryVersion = (root, unit, options, environment, version) => {
507
+ const file = versionFileOf(root, unit, options);
508
+ const versions = VersionsSchema.parse(readJson(file));
509
+ writeJson(file, {
510
+ ...versions,
511
+ [environment]: {
512
+ ...versions[environment],
513
+ version
514
+ }
515
+ });
516
+ return path.relative(root, file);
517
+ };
518
+ const updateRef = ({ tag, environment }) => environment === "preview" ? tag : "main";
519
+ const buildJob = (input) => {
520
+ const { unit, environment } = input;
521
+ return {
522
+ label: `build iOS and Android on EAS ${environment}, submitted when done`,
523
+ local: [{
524
+ command: "pnpm",
525
+ args: [
526
+ "exec",
527
+ "eas",
528
+ "build",
529
+ "--profile",
530
+ environment,
531
+ "--platform",
532
+ "all",
533
+ "--auto-submit",
534
+ "--no-wait"
535
+ ],
536
+ cwd: unit.dir,
537
+ env: {
538
+ EXPO_ATLAS: "false",
539
+ APP_VARIANT: environment
540
+ }
541
+ }]
542
+ };
543
+ };
544
+ const updateJob = (input) => {
545
+ const { unit, environment, options } = input;
546
+ return {
547
+ label: `publish the ${environment} update`,
548
+ local: [{
549
+ command: "pnpm",
550
+ args: ["run", options.update],
551
+ cwd: unit.dir,
552
+ env: { APP_VARIANT: environment }
553
+ }],
554
+ ci: { ref: updateRef(input) }
555
+ };
556
+ };
557
+ /** One eas command on a rollout; no workflow moves a rollout, so it runs here. */
558
+ const rolloutJob = ({ unit, environment }, label, args) => ({
559
+ label,
560
+ local: [{
561
+ command: "pnpm",
562
+ args: [
563
+ "exec",
564
+ "eas",
565
+ ...args,
566
+ "--non-interactive"
567
+ ],
568
+ cwd: unit.dir,
569
+ env: { APP_VARIANT: environment }
570
+ }]
571
+ });
572
+ /** Where an update reaching part of the channel stands, and each way it can go from there. */
573
+ const rolloutChoice = async (context, client, input, update) => {
574
+ const { tag, environment } = input;
575
+ const { group, percentage } = update;
576
+ const launches = await client.updateLaunches(group, environment);
577
+ const hoursOut = update.createdAt ? (context.now().getTime() - new Date(update.createdAt).getTime()) / 36e5 : 0;
578
+ const advice = rolloutAdvice({
579
+ percentage,
580
+ launches: launches.reduce((sum, { installs }) => sum + installs, 0),
581
+ crashes: launches.reduce((sum, { failedInstalls }) => sum + failedInstalls, 0),
582
+ hoursOut
583
+ });
584
+ const suggested = String(advice.suggested);
585
+ const widen = (to) => rolloutJob(input, `widen the ${tag} rollout on ${environment}: ${percentage}% โ†’ ${to}%`, [
586
+ "update:edit",
587
+ group,
588
+ "--rollout-percentage",
589
+ String(to)
590
+ ]);
591
+ const options = [
592
+ ...ROLLOUT_STEPS.filter((step) => step > percentage).map((step) => ({
593
+ value: String(step),
594
+ label: step === 100 ? "Everyone" : `${step}% of people`,
595
+ hint: step === 100 ? "finishes the rollout" : void 0,
596
+ jobs: [widen(step)]
597
+ })),
598
+ {
599
+ value: "stay",
600
+ label: `Stay at ${percentage}%`,
601
+ hint: "nothing changes, check again later",
602
+ jobs: []
603
+ },
604
+ {
605
+ value: "roll-back",
606
+ label: "Roll back",
607
+ hint: "people on it go back to the update before",
608
+ jobs: [rolloutJob(input, `roll back the ${tag} rollout on ${environment}`, [
609
+ "update:revert-update-rollout",
610
+ "--group",
611
+ group
612
+ ])]
613
+ }
614
+ ].map((option) => option.value === suggested ? {
615
+ ...option,
616
+ label: `${option.label} (suggested)`,
617
+ hint: advice.why
618
+ } : option);
619
+ return {
620
+ action: "choose",
621
+ lines: rolloutLines({
622
+ tag,
623
+ environment,
624
+ percentage,
625
+ hoursOut,
626
+ health: updateHealth(launches)
627
+ }),
628
+ question: `How far should ${tag} go on ${environment}?`,
629
+ suggested,
630
+ options,
631
+ held: `${tag} stays at ${percentage}% of people`
632
+ };
633
+ };
634
+ const refuse = (refusal) => ({
635
+ action: "fail",
636
+ ...refusal
637
+ });
638
+ /** The checks a new binary passes before EAS builds it: from main, a new version, a clean tree. */
639
+ const buildRefusal = async (context, analysis, input) => {
640
+ const source = releaseSource("build", input.environment, analysis.git);
641
+ if (!source.allowed) return source.why === "not-main" ? {
642
+ title: `Not from ${analysis.git.branch}`,
643
+ detail: "a binary ships what it is built from, so builds start from main.",
644
+ commands: [
645
+ "git switch main",
646
+ "git pull",
647
+ "shiplane ship"
648
+ ]
649
+ } : {
650
+ title: "main is behind origin",
651
+ detail: "pull it first, a release ships what is merged.",
652
+ commands: ["git pull"]
653
+ };
654
+ if (needsNewVersion(analysis.checked.map(({ state }) => state))) return {
655
+ title: `${analysis.version} already has a ${input.environment} binary`,
656
+ detail: "a new native layer needs a new version, or updates would reach both binaries.",
657
+ commands: ["shiplane"]
658
+ };
659
+ if (!await context.sourceControl.isClean()) return {
660
+ title: "Uncommitted changes",
661
+ detail: "commit or stash them first, a release ships the commit.",
662
+ commands: []
663
+ };
664
+ return null;
665
+ };
666
+ const previewDecision = async (context, input) => {
667
+ const { unit, options } = input;
668
+ const client = easClient(context.shell, path.join(context.root, unit.dir));
669
+ const analysis = await analyse(client, context.sourceControl, "preview", binaryVersion(context.root, unit, options, "preview"), unit.name);
670
+ const version = unit.version;
671
+ const tag = await context.sourceControl.tagCommit(tagName(unit.name, version, context.tagFormat));
672
+ const codeSince = (commit) => tag ? context.changedCode(unit, tag, commit) : [];
673
+ const sinceTag = async (commit) => tag !== null && commit !== null && (await codeSince(commit)).length === 0;
674
+ const builtFrom = analysis.checked.map(({ state }) => state.shipped?.gitCommit ?? null);
675
+ const lastBuildCommit = builtFrom.every((commit) => commit === builtFrom[0]) ? builtFrom[0] ?? null : null;
676
+ const lastUpdate = tag && analysis.plan.action === "update" ? await client.lastUpdate("preview") : null;
677
+ const lastUpdateCommit = lastUpdate ? (await client.updateOrigin(lastUpdate.group, "preview")).commit : null;
678
+ const ship = mobileShip(analysis.plan.action, {
679
+ tagged: tag !== null,
680
+ codeSinceTag: tag !== null && !await sinceTag(analysis.git.head),
681
+ codeShipped: analysis.plan.action === "update" && (await sinceTag(lastBuildCommit) || await sinceTag(lastUpdateCommit))
682
+ });
683
+ const movedPast = ship === "moved-past";
684
+ const kind = analysis.plan.action === "build" ? "build" : "update";
685
+ const named = movedPast ? await codeSince(analysis.git.head) : [];
686
+ const notLocal = movedPast ? { notLocal: `main moved past it (${named.slice(0, 3).join(", ")}${named.length > 3 ? ", ..." : ""} changed since)` } : {};
687
+ switch (movedPast && kind === "update" ? kind : ship) {
688
+ case "build": {
689
+ const refusal = await buildRefusal(context, analysis, input);
690
+ return refusal ? refuse(refusal) : {
691
+ action: "ship",
692
+ kind: "build",
693
+ jobs: [buildJob(input)],
694
+ ...notLocal
695
+ };
696
+ }
697
+ case "update": return {
698
+ action: "ship",
699
+ kind: "update",
700
+ jobs: [updateJob(input)],
701
+ ...notLocal
702
+ };
703
+ case "wait": return {
704
+ action: "none",
705
+ status: `${unit.name} ${version} is building on EAS, submitted when done`
706
+ };
707
+ case "shipped": return {
708
+ action: "none",
709
+ status: `${unit.name} ${version} is on preview`
710
+ };
711
+ case "nothing": return {
712
+ action: "none",
713
+ status: `No ${unit.name} version to ship: add a changeset, then release.`
714
+ };
715
+ case "behind": return refuse({
716
+ title: "main is behind",
717
+ detail: "the last preview build carries a newer version.",
718
+ commands: ["git pull"]
719
+ });
720
+ case "moved-past": {
721
+ const changed = await codeSince(analysis.git.head);
722
+ const named = changed.slice(0, 3).join(", ") + (changed.length > 3 ? ", ..." : "");
723
+ return refuse({
724
+ title: `main moved past ${tagName(unit.name, version, context.tagFormat)}`,
725
+ detail: `a binary or an update ships its commit, and ${named} changed since, code no release named.`,
726
+ commands: ["pnpm changeset", "shiplane"]
727
+ });
728
+ }
729
+ }
730
+ };
731
+ const productionDecision = async (context, input) => {
732
+ const { unit, options } = input;
733
+ const client = easClient(context.shell, path.join(context.root, unit.dir));
734
+ const productionVersion = binaryVersion(context.root, unit, options, "production");
735
+ const analysis = await analyse(client, context.sourceControl, "production", productionVersion, unit.name);
736
+ const version = unit.version;
737
+ const tag = await context.sourceControl.tagCommit(tagName(unit.name, version, context.tagFormat));
738
+ const codeSince = (commit) => tag ? context.changedCode(unit, tag, commit) : [];
739
+ const sinceTag = async (commit) => tag !== null && commit !== null && (await codeSince(commit)).length === 0;
740
+ const action = analysis.plan.action;
741
+ const lastUpdate = tag && action === "update" ? await client.lastUpdate("production") : null;
742
+ const origin = lastUpdate ? await client.updateOrigin(lastUpdate.group, "production") : null;
743
+ const updateCommit = origin?.commit ?? null;
744
+ const shippedCommits = [...analysis.checked.map(({ state }) => state.shipped?.gitCommit ?? null), updateCommit];
745
+ const builtVersion = analysis.checked[0]?.state.shipped?.appVersion ?? null;
746
+ const shippedAny = (await Promise.all(shippedCommits.map(sinceTag))).some(Boolean);
747
+ const rollout = lastUpdate?.rolloutPercentage ?? 100;
748
+ switch (productionMobile(action, {
749
+ tagged: tag !== null,
750
+ sinceTagOnlyVersionFiles: await sinceTag(analysis.git.head),
751
+ expoVersion: version,
752
+ productionVersion,
753
+ codeShipped: action === "submit" || shippedAny,
754
+ builtVersion,
755
+ partialRollout: rollout < 100 && await sinceTag(updateCommit)
756
+ })) {
757
+ case "finish-rollout": return lastUpdate ? rolloutChoice(context, client, input, {
758
+ group: lastUpdate.group,
759
+ percentage: rollout,
760
+ createdAt: origin?.createdAt ?? null
761
+ }) : {
762
+ action: "none",
763
+ status: `${unit.name} ${version} is live`
764
+ };
765
+ case "promote": return {
766
+ action: "pull-request",
767
+ question: `Open the PR that moves production to ${unit.name} ${version}?`,
768
+ branch: `chore/promote-${unit.name}-${version}`,
769
+ title: `chore(release): production builds ${unit.name} ${version}`,
770
+ body: [
771
+ "## ๐ŸŽฏ What & why",
772
+ "",
773
+ `Production builds ${unit.name} ${version}, the version preview validated: \`${path.posix.join(unit.dir, options.versionFile)}\` production moves to it, the runtime version production updates target. Merge it, then \`release --channel production\` builds.`,
774
+ "",
775
+ "## โœ… Testing",
776
+ "",
777
+ "Version file only. No UI change."
778
+ ].join("\n"),
779
+ changesets: [],
780
+ write: () => Promise.resolve([setBinaryVersion(context.root, unit, options, "production", version)])
781
+ };
782
+ case "update": return {
783
+ action: "ship",
784
+ kind: "update",
785
+ jobs: [updateJob(input)]
786
+ };
787
+ case "build": {
788
+ const refusal = await buildRefusal(context, analysis, input);
789
+ return refusal ? refuse(refusal) : {
790
+ action: "ship",
791
+ kind: "build",
792
+ jobs: [buildJob(input)]
793
+ };
794
+ }
795
+ case "wait": return {
796
+ action: "none",
797
+ status: `${unit.name} ${productionVersion} is building on EAS, submitted to the stores when done`
798
+ };
799
+ case "shipped": return {
800
+ action: "none",
801
+ status: `${unit.name} ${builtVersion ?? version} is live`
802
+ };
803
+ case "nothing": return {
804
+ action: "none",
805
+ status: `No ${unit.name} release to promote yet: release to preview first.`
806
+ };
807
+ case "behind": return refuse({
808
+ title: "main is behind",
809
+ detail: "the last production build carries a newer version.",
810
+ commands: ["git pull"]
811
+ });
812
+ case "moved-past": {
813
+ const changed = await codeSince(analysis.git.head);
814
+ const named = changed.slice(0, 3).join(", ") + (changed.length > 3 ? ", ..." : "");
815
+ return refuse({
816
+ title: `main moved past ${tagName(unit.name, version, context.tagFormat)}`,
817
+ detail: `production ships what preview validated, and ${named} changed since, code preview never ran: release it to preview first.`,
818
+ commands: ["pnpm changeset", "shiplane"]
819
+ });
820
+ }
821
+ }
822
+ };
823
+ const ON_MERGE = {
824
+ build: "Build iOS and Android, submitted when done",
825
+ update: "Over-the-air update"
826
+ };
827
+ const easMobileDistribution = () => defineMobileDistribution({
828
+ name: "eas",
829
+ settings: EasOptions,
830
+ decidedBy: {
831
+ platform: "EAS",
832
+ choice: "build or update",
833
+ check: "shiplane status --json"
834
+ },
835
+ typicalMinutes: 8,
836
+ versionFiles: (unit) => [path.posix.join(unit.dir, EasOptions.parse(unit.options).versionFile)],
837
+ describe: ({ environment }) => `EAS ${environment}, iOS and Android`,
838
+ detect: (folder) => {
839
+ if (!folder.has("eas.json")) return null;
840
+ const scripts = z.object({ scripts: z.record(z.string(), z.string()).default({}) }).safeParse(JSON.parse(folder.read("package.json") ?? "{}"));
841
+ const names = scripts.success ? Object.keys(scripts.data.scripts) : [];
842
+ const pick = (environment) => [`eas:update:${environment}`, `eas:update:${environment}:gated`].find((name) => names.includes(name)) ?? names.find((name) => name.includes("update") && name.includes(environment));
843
+ const [preview, production] = [pick("preview"), pick("production")];
844
+ return {
845
+ certainty: "found",
846
+ reason: "eas.json",
847
+ options: preview && production ? { update: {
848
+ preview,
849
+ production
850
+ } } : {}
851
+ };
852
+ },
853
+ questions: (_folder, found) => found.update === void 0 ? [{
854
+ key: "update",
855
+ message: "Package script that publishes an update?",
856
+ initial: "eas:update"
857
+ }] : [],
858
+ requirements: (unit) => [{
859
+ label: "eas",
860
+ check: {
861
+ command: "pnpm",
862
+ args: [
863
+ "exec",
864
+ "eas",
865
+ "whoami"
866
+ ],
867
+ cwd: unit.dir
868
+ },
869
+ fix: {
870
+ command: "pnpm",
871
+ args: [
872
+ "exec",
873
+ "eas",
874
+ "login"
875
+ ],
876
+ cwd: unit.dir
877
+ }
878
+ }],
879
+ jobs: (input) => [buildJob(input), updateJob(input)],
880
+ decide: (context, input) => input.environment === "preview" ? previewDecision(context, input) : productionDecision(context, input),
881
+ inspect: async (context, input) => {
882
+ const { unit, environment, options } = input;
883
+ const client = easClient(context.shell, path.join(context.root, unit.dir));
884
+ const [analysis, lastUpdate] = await Promise.all([analyse(client, context.sourceControl, environment, binaryVersion(context.root, unit, options, environment), unit.name), client.lastUpdate(environment)]);
885
+ const rollout = lastUpdate?.rolloutPercentage ?? 100;
886
+ return {
887
+ ...analysisJson(analysis),
888
+ ...rollout < 100 ? { rolloutPercentage: rollout } : {}
889
+ };
890
+ },
891
+ health: async (context, { unit, tag }) => {
892
+ const client = easClient(context.shell, path.join(context.root, unit.dir));
893
+ const [last, tagged] = await Promise.all([client.lastUpdate("preview"), context.sourceControl.tagCommit(tag)]);
894
+ if (!last || !tagged) return null;
895
+ const { commit } = await client.updateOrigin(last.group, "preview");
896
+ if (!commit || (await context.changedCode(unit, tagged, commit)).length > 0) return null;
897
+ return updateHealth(await client.updateLaunches(last.group, "preview"));
898
+ },
899
+ beforeVersion: async (context, input) => {
900
+ const { unit, options, bump } = input;
901
+ const analysis = await analyse(easClient(context.shell, path.join(context.root, unit.dir)), context.sourceControl, "preview", binaryVersion(context.root, unit, options, "preview"), unit.name);
902
+ const forced = input.chosen === "build" && analysis.plan.action !== "build";
903
+ const mobile = mobileRelease(forced ? "build" : analysis.plan.action, bump, unit.version);
904
+ const summary = forced ? ["โ†‘ build asked for, an update would do", ...verdictLines(analysis)] : verdictLines(analysis);
905
+ if (mobile.ships === "blocked") return {
906
+ blocked: {
907
+ title: "No release",
908
+ detail: mobile.why === "build-running" ? "a preview build is still running: release once it's done, the native layer it carries decides the version." : "main is behind the last preview build's version.",
909
+ commands: ["shiplane"]
910
+ },
911
+ added: [],
912
+ writes: [],
913
+ summary,
914
+ onMerge: "Nothing ships"
915
+ };
916
+ const changes = [...new Set(analysis.checked.flatMap(({ changes: lines }) => lines))];
917
+ const version = mobile.ships === "build" || mobile.ships === "update" ? mobile.version : unit.version;
918
+ return {
919
+ choice: mobileChoice(analysis.plan.action, planWhy(analysis), unit.name),
920
+ added: releaseChangesets(mobile, changes, unit.packageName),
921
+ writes: mobile.ships === "build" ? [{
922
+ label: `preview app version โ†’ ${version}`,
923
+ run: async () => {
924
+ setBinaryVersion(context.root, unit, options, "preview", version);
925
+ await Promise.resolve();
926
+ }
927
+ }] : [],
928
+ summary: mobile.ships === "nothing" ? ["โ€“ nothing changed"] : summary,
929
+ onMerge: ON_MERGE[mobile.ships] ?? "Nothing ships",
930
+ ...mobile.ships === "build" ? { step: `Build iOS and Android ${version} on EAS, submitted to TestFlight and Play internal when done` } : mobile.ships === "update" ? { step: `Publish ${version} over the air to preview` } : {}
931
+ };
932
+ }
933
+ });
934
+ //#endregion
935
+ export { easMobileDistribution };
936
+
937
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/eas/policy.ts","../src/eas/analyse.ts","../src/eas/client.ts","../src/eas-mobile-distribution.adapter.ts"],"sourcesContent":["import type { AppBump, Environment, PlannedChangeset, VersionChoice } from \"shiplane/plugin\";\nimport { compareVersions, nextPatch } from \"shiplane/plugin\";\n\nexport type Platform = \"ios\" | \"android\";\n\nexport interface ShippedBuild {\n id: string;\n appVersion: string | null;\n buildNumber: string | null;\n fingerprint: string | null;\n gitCommit: string | null;\n createdAt: string;\n}\n\nexport interface RunningBuild {\n id: string;\n appVersion: string | null;\n buildNumber: string | null;\n}\n\nexport interface PlatformState {\n platform: Platform;\n /** This commit: its native fingerprint (the runtime version updates target), app version and sha. */\n local: { fingerprint: string; version: string; commit: string };\n /** The newest finished store build on the channel, or null when there is none. */\n shipped: ShippedBuild | null;\n /** The newest store build on the channel still queued or running. */\n running: RunningBuild | null;\n}\n\nexport type BuildReason = \"no-build\" | \"no-fingerprint\" | \"native-changed\" | \"version-changed\";\n\nexport type PlatformVerdict =\n | { platform: Platform; action: \"update\"; shipped: ShippedBuild }\n | { platform: Platform; action: \"submit\"; shipped: ShippedBuild }\n | {\n platform: Platform;\n action: \"pull\";\n why: \"version-behind\";\n shipped: ShippedBuild;\n /** The newest version built on the channel, whose bump this branch misses. */\n newerVersion: string;\n }\n | { platform: Platform; action: \"wait\"; why: \"build-running\"; running: RunningBuild }\n | { platform: Platform; action: \"build\"; why: BuildReason; shipped: ShippedBuild | null };\n\nexport type ReleaseAction = PlatformVerdict[\"action\"];\n\n/** What this commit needs on one platform, given the builds on its channel. */\nexport const platformVerdict = ({\n platform,\n local,\n shipped,\n running,\n}: PlatformState): PlatformVerdict => {\n if (running?.appVersion === local.version) {\n return { platform, action: \"wait\", why: \"build-running\", running };\n }\n // A running build of a newer version means its bump commit exists: this branch is behind it.\n const newestVersion = running?.appVersion ?? shipped?.appVersion;\n if (shipped && newestVersion && compareVersions(local.version, newestVersion) < 0) {\n return {\n platform,\n action: \"pull\",\n why: \"version-behind\",\n shipped,\n newerVersion: newestVersion,\n };\n }\n if (!shipped) return { platform, action: \"build\", why: \"no-build\", shipped };\n\n // The version is part of the fingerprint and names the binary: it decides first.\n if (shipped.appVersion && shipped.appVersion !== local.version) {\n return { platform, action: \"build\", why: \"version-changed\", shipped };\n }\n if (!shipped.fingerprint) return { platform, action: \"build\", why: \"no-fingerprint\", shipped };\n if (shipped.fingerprint !== local.fingerprint) {\n return { platform, action: \"build\", why: \"native-changed\", shipped };\n }\n if (shipped.gitCommit === local.commit) return { platform, action: \"submit\", shipped };\n return { platform, action: \"update\", shipped };\n};\n\nexport interface ReleasePlan {\n action: ReleaseAction;\n commands: string[];\n}\n\n// One update reaches every platform, so the most demanding platform decides; a stale branch\n// is caught up first, since every other verdict is about an older version.\nconst PRECEDENCE: ReleaseAction[] = [\"pull\", \"build\", \"wait\", \"submit\", \"update\"];\n\n/** The next step for the whole release, with the release CLI commands that take it. */\nexport const releasePlan = (\n channel: Environment,\n verdicts: PlatformVerdict[],\n unit: string,\n): ReleasePlan => {\n const action =\n PRECEDENCE.find((candidate) => verdicts.some((verdict) => verdict.action === candidate)) ??\n \"update\";\n const flag = channel === \"preview\" ? \"\" : ` --channel ${channel}`;\n const COMMANDS: Record<ReleaseAction, string[]> = {\n // EAS submits a build when it is done: shipping it is one command.\n build: [`pnpm shiplane ship ${unit}${flag}`],\n // The release that versioned the build is on main: the branch catches up from there.\n pull: [\"git fetch origin\", \"git merge origin/main\", `pnpm shiplane${flag}`],\n wait: [`pnpm shiplane status${flag}`],\n // The build made from this commit exists and EAS submitted it.\n submit: [`pnpm shiplane status${flag}`],\n update: [`pnpm shiplane ship ${unit}${flag}`],\n };\n\n return { action, commands: COMMANDS[action] };\n};\n\n/**\n * A build of a version some binary already carries would give two native layers one version: each\n * binary keeps its own. The release pull request, or the promotion one, moves it first.\n */\nexport const needsNewVersion = (states: PlatformState[]) =>\n states.some(({ local, shipped }) => shipped?.appVersion === local.version);\n\nexport type MobileRelease =\n | { ships: \"build\"; version: string; nativeChangeset: boolean }\n | { ships: \"update\"; version: string }\n | { ships: \"nothing\" }\n | { ships: \"blocked\"; why: \"build-running\" | \"behind\" };\n\n/**\n * What the release does for mobile, from preview's verdict. A new native layer gets a new\n * version, so each binary's version names one native layer.\n */\nexport const mobileRelease = (\n action: ReleaseAction,\n expo: AppBump | undefined,\n currentVersion: string,\n): MobileRelease => {\n if (action === \"wait\") return { ships: \"blocked\", why: \"build-running\" };\n if (action === \"pull\") return { ships: \"blocked\", why: \"behind\" };\n if (action === \"build\") {\n return expo\n ? { ships: \"build\", version: expo.to, nativeChangeset: false }\n : { ships: \"build\", version: nextPatch(currentVersion), nativeChangeset: true };\n }\n return expo ? { ships: \"update\", version: expo.to } : { ships: \"nothing\" };\n};\n\n/** The changeset the release writes itself: a build gets a version even with no expo changeset. */\nexport const releaseChangesets = (\n mobile: MobileRelease,\n changes: string[],\n app: string,\n): PlannedChangeset[] =>\n mobile.ships === \"build\" && mobile.nativeChangeset\n ? [\n {\n id: `native-${mobile.version}`,\n releases: [{ name: app, type: \"patch\" }],\n summary:\n changes.length > 0\n ? `Native code changed: ${changes.map((change) => change.replace(/^[+~-] /, \"\")).join(\", \")}.`\n : \"A new build of the app, with the same native code.\",\n },\n ]\n : [];\n\nconst BUILD_LABEL = \"New build, iOS and Android\";\nconst UPDATE_LABEL = \"Over-the-air update\";\n\n/**\n * What the person may pick once detection spoke, the suggestion first: a build is always possible,\n * an update only on the native layer the installed binaries carry.\n */\nexport const mobileChoice = (\n action: ReleaseAction,\n why: string,\n app: string,\n): VersionChoice | undefined => {\n if (action === \"wait\" || action === \"pull\") return undefined;\n const question = `What should ${app} ship to preview?`;\n if (action === \"build\") {\n return {\n question,\n suggested: \"build\",\n options: [\n { value: \"build\", label: `${BUILD_LABEL} (suggested)`, hint: why },\n {\n value: \"update\",\n label: UPDATE_LABEL,\n hint: \"not safe: installed binaries lack this native layer\",\n disabled: true,\n },\n ],\n };\n }\n return {\n question,\n suggested: \"update\",\n options: [\n { value: \"update\", label: `${UPDATE_LABEL} (suggested)`, hint: why },\n {\n value: \"build\",\n label: BUILD_LABEL,\n hint: \"not needed: a new binary anyway, under its own version\",\n },\n ],\n };\n};\n\nexport type MobileShip =\n \"build\" | \"update\" | \"wait\" | \"shipped\" | \"moved-past\" | \"behind\" | \"nothing\";\n\n/**\n * What mobile still needs for the expo version a release tagged. A binary or an update ships its\n * commit: once the app's code moved past the release, it would carry code no release named.\n */\nexport const mobileShip = (\n action: ReleaseAction,\n state: {\n /** The current expo version has a release tag. */\n tagged: boolean;\n /** Code reaching the app changed between that tag and main. */\n codeSinceTag: boolean;\n /** The last build or update shipped the tagged code. */\n codeShipped: boolean;\n },\n): MobileShip => {\n if (!state.tagged) return \"nothing\";\n if (action === \"wait\") return \"wait\";\n if (action === \"pull\") return \"behind\";\n if (action === \"submit\") return \"shipped\";\n if (action === \"update\" && state.codeShipped) return \"shipped\";\n if (state.codeSinceTag) return \"moved-past\";\n return action;\n};\n\nexport type ProductionMobile =\n | \"promote\"\n | \"finish-rollout\"\n | \"build\"\n | \"update\"\n | \"wait\"\n | \"shipped\"\n | \"moved-past\"\n | \"behind\"\n | \"nothing\";\n\n/**\n * What production still needs to run the expo version preview validated. A new native layer needs\n * production's version moved first (the promotion PR).\n */\nexport const productionMobile = (\n action: ReleaseAction,\n state: {\n /** The current expo version has a release tag. */\n tagged: boolean;\n /** Main differs from that tag by version files at most. */\n sinceTagOnlyVersionFiles: boolean;\n expoVersion: string;\n /** apps/expo/app-version.json production: the binary version production builds carry. */\n productionVersion: string;\n /** The last production build or update shipped the tagged code. */\n codeShipped: boolean;\n /** The version of the last finished production build. */\n builtVersion: string | null;\n /** The last update, made from the tagged code, reaches only part of production. */\n partialRollout: boolean;\n },\n): ProductionMobile => {\n if (!state.tagged) return \"nothing\";\n if (action === \"wait\") return \"wait\";\n if (action === \"pull\") return \"behind\";\n if (action !== \"build\" && state.codeShipped) {\n return state.partialRollout ? \"finish-rollout\" : \"shipped\";\n }\n if (!state.sinceTagOnlyVersionFiles) return \"moved-past\";\n if (action === \"build\") {\n return state.productionVersion === state.expoVersion ? \"build\" : \"promote\";\n }\n return \"update\";\n};\n\nconst MOBILE_SHIPS: Partial<Record<MobileRelease[\"ships\"], string>> = {\n build: \"Build iOS and Android, submitted when done\",\n update: \"Over-the-air update\",\n};\n\n/** What happens to each unit once the release pull request is merged, the mobile app's from EAS. */\nexport const onMerge = (mobile: MobileRelease, mobileApp: string) => (bump: AppBump) => {\n if (bump.app === mobileApp) return MOBILE_SHIPS[mobile.ships] ?? \"Nothing ships\";\n const reason = bump.movedWith.length > 0 ? ` (moved with ${bump.movedWith.join(\", \")})` : \"\";\n return `Deploy preview${reason}`;\n};\n\n/** The shares of people a production rollout widens through, in order. */\nexport const ROLLOUT_STEPS = [10, 25, 50, 100] as const;\n\nconst counted = (total: number, one: string, many: string) =>\n `${total} ${total === 1 ? one : many}`;\n\n/** How long an update has been out, as a person says it: hours under two days, then days. */\nexport const timeOut = (hours: number) =>\n hours < 48\n ? counted(Math.max(1, Math.floor(hours)), \"hour\", \"hours\")\n : counted(Math.floor(hours / 24), \"day\", \"days\");\n\n/**\n * The next move for an update reaching part of production: hold while it crashed, nobody ran it or\n * it is under a day old; otherwise the next step up.\n */\nexport const rolloutAdvice = ({\n percentage,\n launches,\n crashes,\n hoursOut,\n}: {\n percentage: number;\n launches: number;\n crashes: number;\n hoursOut: number;\n}): { suggested: number | \"stay\"; why: string } => {\n if (crashes > 0) {\n return {\n suggested: \"stay\",\n why: `${counted(crashes, \"crash\", \"crashes\")} since it went out: look before widening, or roll back`,\n };\n }\n if (launches === 0) {\n return { suggested: \"stay\", why: \"nobody has launched it yet: wait for launches\" };\n }\n if (hoursOut < 24)\n return { suggested: \"stay\", why: `out for ${timeOut(hoursOut)}: give it a day` };\n return {\n suggested: ROLLOUT_STEPS.find((step) => step > percentage) ?? 100,\n why: `healthy for ${timeOut(hoursOut)}: widen it`,\n };\n};\n","import { styleText } from \"node:util\";\nimport type { Environment, GitPosition, Health, SourceControlPort } from \"shiplane/plugin\";\nimport { progressBar } from \"shiplane/plugin\";\n\nimport type { easClient } from \"./client\";\nimport type {\n Platform,\n PlatformState,\n PlatformVerdict,\n ReleaseAction,\n ReleasePlan,\n} from \"./policy\";\nimport { platformVerdict, releasePlan, timeOut } from \"./policy\";\n\nexport const PLATFORMS: Platform[] = [\"ios\", \"android\"];\n\nexport const PLATFORM_NAME: Record<Platform, string> = { ios: \"iOS\", android: \"Android\" };\n\nexport interface Analysis {\n channel: Environment;\n version: string;\n commit: string;\n git: GitPosition;\n checked: { state: PlatformState; verdict: PlatformVerdict; changes: string[] }[];\n plan: ReleasePlan;\n}\n\n/** Each platform's last store build on the channel against this commit's native fingerprint. */\nexport const analyse = async (\n client: ReturnType<typeof easClient>,\n source: SourceControlPort,\n environment: Environment,\n version: string,\n unit: string,\n): Promise<Analysis> => {\n const position = await source.position();\n const checked = await Promise.all(\n PLATFORMS.map(async (platform) => {\n const [fingerprint, builds] = await Promise.all([\n client.localFingerprint(platform, environment),\n client.channelBuilds(platform, environment),\n ]);\n const state: PlatformState = {\n platform,\n local: { fingerprint, version, commit: position.head },\n ...builds,\n };\n const verdict = platformVerdict(state);\n const changes =\n verdict.action === \"build\" && verdict.why === \"native-changed\" && verdict.shipped\n ? await client.nativeChanges(verdict.shipped.id, environment)\n : [];\n return { state, verdict, changes };\n }),\n );\n return {\n channel: environment,\n version,\n commit: position.head,\n git: position,\n checked,\n plan: releasePlan(\n environment,\n checked.map(({ verdict }) => verdict),\n unit,\n ),\n };\n};\n\nconst LOOK: Record<ReleaseAction, { mark: string; style: \"green\" | \"yellow\" | \"red\" }> = {\n update: { mark: \"โœ“\", style: \"green\" },\n submit: { mark: \"โ†‘\", style: \"green\" },\n wait: { mark: \"โ€ฆ\", style: \"yellow\" },\n pull: { mark: \"โ†“\", style: \"yellow\" },\n build: { mark: \"โœ—\", style: \"red\" },\n};\n\n/** Why a platform needs what it needs, as a person reads it. */\nexport const why = (verdict: PlatformVerdict): string => {\n switch (verdict.action) {\n case \"update\":\n return \"same native layer and version as the last build\";\n case \"submit\":\n return \"the last build was made from this commit\";\n case \"wait\":\n return `build ${verdict.running.buildNumber ?? \"?\"} for this version is still running`;\n case \"pull\":\n return `this branch misses the ${verdict.newerVersion} release of a build on this channel`;\n case \"build\":\n // Builds that errored leave no binary: the last finished one is what devices run.\n return {\n \"no-build\": \"no store build on this channel yet\",\n \"no-fingerprint\": \"the last build has no fingerprint, nothing proves an update fits\",\n \"native-changed\": \"native code changed since the last build\",\n \"version-changed\": `no binary carries this version yet: the last finished build is ${verdict.shipped?.appVersion ?? \"older\"}`,\n }[verdict.why];\n }\n};\n\n/** Why the deciding platform needs what the plan says, or nothing when none decided. */\nexport const planWhy = ({ checked, plan }: Analysis) => {\n const shown = checked.find(({ verdict }) => verdict.action === plan.action);\n return shown ? why(shown.verdict) : \"\";\n};\n\nconst MAX_CHANGES = 6;\n\n/** The verdict line a summary shows for the platform that decides, with what changed under it. */\nexport const verdictLines = ({ checked, plan }: Analysis): string[] => {\n const shown = checked.find(({ verdict }) => verdict.action === plan.action);\n if (!shown) return [];\n const look = LOOK[shown.verdict.action];\n return [\n `${styleText(look.style, `${look.mark} ${shown.verdict.action}`)} ${styleText(\"dim\", why(shown.verdict))}`,\n ...shown.changes.slice(0, MAX_CHANGES).map((change) => styleText(\"dim\", ` ${change}`)),\n ];\n};\n\n/** The analysis as `status --json` prints it, the same shape the old release check printed. */\nexport const analysisJson = ({ channel, version, commit, plan, checked }: Analysis) => ({\n channel,\n version,\n commit,\n ...plan,\n platforms: checked.map(({ verdict, changes }) => ({ ...verdict, changes })),\n});\n\nconst count = (total: number, one: string, many: string) => `${total} ${total === 1 ? one : many}`;\n\n/** An update's health from its launches: unwell once it crashed, or while nobody has run it. */\nexport const updateHealth = (\n launches: { platform: Platform; installs: number; failedInstalls: number }[],\n): Health => {\n const summary = launches\n .map(\n ({ platform, installs, failedInstalls }) =>\n `${PLATFORM_NAME[platform]} ${count(installs, \"launch\", \"launches\")}, ${count(failedInstalls, \"crash\", \"crashes\")}`,\n )\n .join(\" ยท \");\n const crashes = launches.reduce((sum, { failedInstalls }) => sum + failedInstalls, 0);\n if (crashes > 0) {\n return { ok: false, summary, why: `${count(crashes, \"crash\", \"crashes\")} on preview` };\n }\n if (launches.every(({ installs }) => installs === 0)) {\n return { ok: false, summary, why: \"nobody has launched it on preview yet\" };\n }\n return { ok: true, summary };\n};\n\n/** Where a rollout stands, as the lines above the question: whose, how far, how long, how it does. */\nexport const rolloutLines = ({\n tag,\n environment,\n percentage,\n hoursOut,\n health,\n}: {\n tag: string;\n environment: Environment;\n percentage: number;\n hoursOut: number;\n health: Health;\n}) => [\n `${styleText(\"bold\", tag)} ${styleText(\"dim\", `on ${environment}`)}`,\n `${styleText(health.ok ? \"green\" : \"yellow\", progressBar(percentage, 100, 20))} ${percentage}% of people ยท out for ${timeOut(hoursOut)}`,\n styleText(\"dim\", health.summary),\n];\n","import type { Environment, Shell } from \"shiplane/plugin\";\nimport { z } from \"shiplane/plugin\";\n\nimport type { Platform, RunningBuild, ShippedBuild } from \"./policy\";\n\n// eas-cli prints notices around its JSON: keep the outermost JSON value only.\nconst parseEasJson = (output: string): unknown => {\n const start = output.search(/[[{]/);\n const end = Math.max(output.lastIndexOf(\"]\"), output.lastIndexOf(\"}\"));\n return JSON.parse(output.slice(start, end + 1));\n};\n\nconst SourceSchema = z.object({\n type: z.string(),\n filePath: z.string().optional(),\n id: z.string().optional(),\n hash: z.string().nullish(),\n});\nconst FingerprintSchema = z.object({ hash: z.string(), sources: z.array(SourceSchema) });\nconst EasBuildSchema = z.object({\n id: z.string(),\n status: z.string(),\n buildProfile: z.string().nullish(),\n isForIosSimulator: z.boolean().nullish(),\n appVersion: z.string().nullish(),\n appBuildVersion: z.string().nullish(),\n gitCommitHash: z.string().nullish(),\n createdAt: z.string(),\n fingerprint: z.object({ hash: z.string() }).nullish(),\n});\nconst UpdateListSchema = z.object({\n currentPage: z.array(\n // EAS lists rolloutPercentage only while a rollout holds the group back from part of the channel.\n z.object({ group: z.string(), rolloutPercentage: z.number().optional() }),\n ),\n});\nconst UpdateViewSchema = z.array(\n z.object({ gitCommitHash: z.string().nullish(), createdAt: z.string().nullish() }),\n);\nconst UpdateInsightsSchema = z.object({\n platforms: z.array(\n z.object({\n platform: z.enum([\"ios\", \"android\"]),\n totals: z.object({ installs: z.number(), failedInstalls: z.number() }),\n }),\n ),\n});\n\nconst RUNNING = new Set([\"NEW\", \"IN_QUEUE\", \"IN_PROGRESS\"]);\n\n// eas-cli rejects --non-interactive on these commands as a nonexistent flag.\nconst WITHOUT_NON_INTERACTIVE_FLAG = new Set([\"update:view\"]);\n\n// The first stderr line is often eas-cli's upgrade notice: report its own error line instead.\nconst failureReason = (stderr: string): string => {\n const lines = stderr\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter(Boolean);\n return lines.find((line) => line.startsWith(\"Error:\")) ?? lines.at(-1) ?? \"\";\n};\n\n/** The eas CLI, run in the app's folder with its environment as APP_VARIANT. */\nexport const easClient = (shell: Shell, cwd: string) => {\n const eas = async (args: string[], environment: Environment): Promise<unknown> => {\n const command = args[0] ?? \"\";\n const flags = WITHOUT_NON_INTERACTIVE_FLAG.has(command)\n ? [\"--json\"]\n : [\"--json\", \"--non-interactive\"];\n const { status, stdout, stderr } = await shell.capture(\n \"pnpm\",\n [\"exec\", \"eas\", ...args, ...flags],\n {\n cwd,\n env: { APP_VARIANT: environment },\n },\n );\n if (status !== 0) {\n throw new Error(`eas ${command} exited with ${status}: ${failureReason(stderr)}`);\n }\n return parseEasJson(stdout);\n };\n\n return {\n localFingerprint: async (platform: Platform, environment: Environment) =>\n FingerprintSchema.parse(\n await eas([\"fingerprint:generate\", \"--platform\", platform], environment),\n ).hash,\n\n // The simulator profile shares the preview channel: only the channel's own store profile counts.\n channelBuilds: async (platform: Platform, environment: Environment) => {\n const builds = z\n .array(EasBuildSchema)\n .parse(await eas([\"build:list\", \"--platform\", platform, \"--limit\", \"30\"], environment))\n .filter((build) => build.buildProfile === environment && !build.isForIosSimulator);\n const finished = builds.find((build) => build.status === \"FINISHED\");\n const running = builds.find((build) => RUNNING.has(build.status));\n return {\n shipped: finished\n ? ({\n id: finished.id,\n appVersion: finished.appVersion ?? null,\n buildNumber: finished.appBuildVersion ?? null,\n fingerprint: finished.fingerprint?.hash ?? null,\n gitCommit: finished.gitCommitHash ?? null,\n createdAt: finished.createdAt,\n } satisfies ShippedBuild)\n : null,\n running: running\n ? ({\n id: running.id,\n appVersion: running.appVersion ?? null,\n buildNumber: running.appBuildVersion ?? null,\n } satisfies RunningBuild)\n : null,\n };\n },\n\n // What differs between the last build's native layer and this commit's, for a \"native changed\".\n nativeChanges: async (buildId: string, environment: Environment): Promise<string[]> => {\n const { fingerprint1, fingerprint2 } = z\n .object({ fingerprint1: FingerprintSchema, fingerprint2: FingerprintSchema })\n .parse(await eas([\"fingerprint:compare\", \"--build-id\", buildId], environment));\n type Source = z.infer<typeof SourceSchema>;\n const key = (source: Source) => `${source.type}:${source.filePath ?? source.id ?? \"\"}`;\n const label = (source: Source) =>\n (source.filePath ?? source.id ?? source.type).replace(/^(\\.\\.\\/)+/, \"\");\n const before = new Map(fingerprint1.sources.map((source) => [key(source), source]));\n const after = new Map(fingerprint2.sources.map((source) => [key(source), source]));\n return [\n ...[...after].flatMap(([id, source]) => {\n const old = before.get(id);\n if (!old) return [`+ ${label(source)}`];\n return old.hash === source.hash ? [] : [`~ ${label(source)}`];\n }),\n ...[...before].flatMap(([id, source]) => (after.has(id) ? [] : [`- ${label(source)}`])),\n ];\n },\n\n /** The newest update group on the channel, with the share of people it reaches. */\n lastUpdate: async (\n environment: Environment,\n ): Promise<{ group: string; rolloutPercentage: number } | null> => {\n const { currentPage } = UpdateListSchema.parse(\n await eas([\"update:list\", \"--branch\", environment, \"--limit\", \"1\"], environment),\n );\n const [last] = currentPage;\n return last ? { group: last.group, rolloutPercentage: last.rolloutPercentage ?? 100 } : null;\n },\n\n /** Each platform's launches of an update group over the last 7 days, and the failed ones. */\n updateLaunches: async (group: string, environment: Environment) =>\n UpdateInsightsSchema.parse(await eas([\"update:insights\", group], environment)).platforms.map(\n ({ platform, totals }) => ({ platform, ...totals }),\n ),\n\n /** The commit an update group was made from, and when it was published. */\n updateOrigin: async (group: string, environment: Environment) => {\n const [first] = UpdateViewSchema.parse(await eas([\"update:view\", group], environment));\n return { commit: first?.gitCommitHash ?? null, createdAt: first?.createdAt ?? null };\n },\n };\n};\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type {\n DecideContext,\n Decision,\n DeliveryAdapter,\n Environment,\n Job,\n JobInput,\n MobileDistributionPort,\n Refusal,\n ReleaseUnit,\n Resolved,\n} from \"shiplane/plugin\";\nimport { defineMobileDistribution, perEnv, releaseSource, tagName, z } from \"shiplane/plugin\";\n\nimport type { MobileRelease } from \"./eas/policy\";\nimport {\n analyse,\n analysisJson,\n planWhy,\n rolloutLines,\n updateHealth,\n verdictLines,\n} from \"./eas/analyse\";\nimport { easClient } from \"./eas/client\";\nimport {\n mobileChoice,\n mobileRelease,\n mobileShip,\n needsNewVersion,\n productionMobile,\n releaseChangesets,\n ROLLOUT_STEPS,\n rolloutAdvice,\n} from \"./eas/policy\";\n\nconst EasOptions = z.object({\n versionFile: z.string().default(\"app-version.json\").describe(\"Each environment's binary version\"),\n update: perEnv(z.string()).describe(\n \"The package script that publishes an update, per environment\",\n ),\n});\n\ntype Options = Resolved<z.output<typeof EasOptions>>;\n\nconst readJson = (file: string): Record<string, unknown> =>\n z.record(z.string(), z.unknown()).parse(JSON.parse(fs.readFileSync(file, \"utf8\")));\n\nconst writeJson = (file: string, value: unknown) =>\n fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\\n`);\n\nconst VersionsSchema = z.record(z.string(), z.object({ version: z.string() }).loose());\n\n/** The binary version an environment carries. */\nconst versionFileOf = (root: string, unit: ReleaseUnit, options: Options) =>\n path.join(root, unit.dir, options.versionFile);\n\nconst binaryVersion = (\n root: string,\n unit: ReleaseUnit,\n options: Options,\n environment: Environment,\n) =>\n VersionsSchema.parse(readJson(versionFileOf(root, unit, options)))[environment]?.version ??\n unit.version;\n\nconst setBinaryVersion = (\n root: string,\n unit: ReleaseUnit,\n options: Options,\n environment: Environment,\n version: string,\n) => {\n const file = versionFileOf(root, unit, options);\n const versions = VersionsSchema.parse(readJson(file));\n writeJson(file, { ...versions, [environment]: { ...versions[environment], version } });\n return path.relative(root, file);\n};\n\n// A build runs on EAS from this machine; a preview update ships what its tag named, production main.\nconst updateRef = ({ tag, environment }: JobInput<Options>) =>\n environment === \"preview\" ? tag : \"main\";\n\nconst buildJob = (input: JobInput<Options>): Job => {\n const { unit, environment } = input;\n return {\n label: `build iOS and Android on EAS ${environment}, submitted when done`,\n local: [\n {\n command: \"pnpm\",\n args: [\n \"exec\",\n \"eas\",\n \"build\",\n \"--profile\",\n environment,\n \"--platform\",\n \"all\",\n \"--auto-submit\",\n \"--no-wait\",\n ],\n cwd: unit.dir,\n env: { EXPO_ATLAS: \"false\", APP_VARIANT: environment },\n },\n ],\n };\n};\n\nconst updateJob = (input: JobInput<Options>): Job => {\n const { unit, environment, options } = input;\n return {\n label: `publish the ${environment} update`,\n local: [\n {\n command: \"pnpm\",\n args: [\"run\", options.update],\n cwd: unit.dir,\n env: { APP_VARIANT: environment },\n },\n ],\n ci: { ref: updateRef(input) },\n };\n};\n\n/** One eas command on a rollout; no workflow moves a rollout, so it runs here. */\nconst rolloutJob = (\n { unit, environment }: JobInput<Options>,\n label: string,\n args: string[],\n): Job => ({\n label,\n local: [\n {\n command: \"pnpm\",\n args: [\"exec\", \"eas\", ...args, \"--non-interactive\"],\n cwd: unit.dir,\n env: { APP_VARIANT: environment },\n },\n ],\n});\n\n/** Where an update reaching part of the channel stands, and each way it can go from there. */\nconst rolloutChoice = async (\n context: DecideContext,\n client: ReturnType<typeof easClient>,\n input: JobInput<Options>,\n update: { group: string; percentage: number; createdAt: string | null },\n): Promise<Decision> => {\n const { tag, environment } = input;\n const { group, percentage } = update;\n const launches = await client.updateLaunches(group, environment);\n const hoursOut = update.createdAt\n ? (context.now().getTime() - new Date(update.createdAt).getTime()) / 3_600_000\n : 0;\n const advice = rolloutAdvice({\n percentage,\n launches: launches.reduce((sum, { installs }) => sum + installs, 0),\n crashes: launches.reduce((sum, { failedInstalls }) => sum + failedInstalls, 0),\n hoursOut,\n });\n const suggested = String(advice.suggested);\n const widen = (to: number) =>\n rolloutJob(input, `widen the ${tag} rollout on ${environment}: ${percentage}% โ†’ ${to}%`, [\n \"update:edit\",\n group,\n \"--rollout-percentage\",\n String(to),\n ]);\n const options = [\n ...ROLLOUT_STEPS.filter((step) => step > percentage).map((step) => ({\n value: String(step),\n label: step === 100 ? \"Everyone\" : `${step}% of people`,\n hint: step === 100 ? \"finishes the rollout\" : undefined,\n jobs: [widen(step)],\n })),\n {\n value: \"stay\",\n label: `Stay at ${percentage}%`,\n hint: \"nothing changes, check again later\",\n jobs: [],\n },\n {\n value: \"roll-back\",\n label: \"Roll back\",\n hint: \"people on it go back to the update before\",\n jobs: [\n rolloutJob(input, `roll back the ${tag} rollout on ${environment}`, [\n \"update:revert-update-rollout\",\n \"--group\",\n group,\n ]),\n ],\n },\n ].map((option) =>\n option.value === suggested\n ? { ...option, label: `${option.label} (suggested)`, hint: advice.why }\n : option,\n );\n return {\n action: \"choose\",\n lines: rolloutLines({\n tag,\n environment,\n percentage,\n hoursOut,\n health: updateHealth(launches),\n }),\n question: `How far should ${tag} go on ${environment}?`,\n suggested,\n options,\n held: `${tag} stays at ${percentage}% of people`,\n };\n};\n\nconst refuse = (refusal: Refusal): Decision => ({ action: \"fail\", ...refusal });\n\n/** The checks a new binary passes before EAS builds it: from main, a new version, a clean tree. */\nconst buildRefusal = async (\n context: DecideContext,\n analysis: Awaited<ReturnType<typeof analyse>>,\n input: JobInput<Options>,\n): Promise<Refusal | null> => {\n const source = releaseSource(\"build\", input.environment, analysis.git);\n if (!source.allowed) {\n return source.why === \"not-main\"\n ? {\n title: `Not from ${analysis.git.branch}`,\n detail: \"a binary ships what it is built from, so builds start from main.\",\n commands: [\"git switch main\", \"git pull\", \"shiplane ship\"],\n }\n : {\n title: \"main is behind origin\",\n detail: \"pull it first, a release ships what is merged.\",\n commands: [\"git pull\"],\n };\n }\n if (needsNewVersion(analysis.checked.map(({ state }) => state))) {\n return {\n title: `${analysis.version} already has a ${input.environment} binary`,\n detail: \"a new native layer needs a new version, or updates would reach both binaries.\",\n commands: [\"shiplane\"],\n };\n }\n if (!(await context.sourceControl.isClean())) {\n return {\n title: \"Uncommitted changes\",\n detail: \"commit or stash them first, a release ships the commit.\",\n commands: [],\n };\n }\n return null;\n};\n\nconst previewDecision = async (\n context: DecideContext,\n input: JobInput<Options>,\n): Promise<Decision> => {\n const { unit, options } = input;\n const client = easClient(context.shell, path.join(context.root, unit.dir));\n const analysis = await analyse(\n client,\n context.sourceControl,\n \"preview\",\n binaryVersion(context.root, unit, options, \"preview\"),\n unit.name,\n );\n const version = unit.version;\n const tag = await context.sourceControl.tagCommit(tagName(unit.name, version, context.tagFormat));\n const codeSince = (commit: string) => (tag ? context.changedCode(unit, tag, commit) : []);\n const sinceTag = async (commit: string | null) =>\n tag !== null && commit !== null && (await codeSince(commit)).length === 0;\n const builtFrom = analysis.checked.map(({ state }) => state.shipped?.gitCommit ?? null);\n const lastBuildCommit = builtFrom.every((commit) => commit === builtFrom[0])\n ? (builtFrom[0] ?? null)\n : null;\n const lastUpdate =\n tag && analysis.plan.action === \"update\" ? await client.lastUpdate(\"preview\") : null;\n const lastUpdateCommit = lastUpdate\n ? (await client.updateOrigin(lastUpdate.group, \"preview\")).commit\n : null;\n const ship = mobileShip(analysis.plan.action, {\n tagged: tag !== null,\n codeSinceTag: tag !== null && !(await sinceTag(analysis.git.head)),\n codeShipped:\n analysis.plan.action === \"update\" &&\n ((await sinceTag(lastBuildCommit)) || (await sinceTag(lastUpdateCommit))),\n });\n // Main moved past the tag: GitHub Actions still ships the tag, this machine only its checkout.\n const movedPast = ship === \"moved-past\";\n const kind = analysis.plan.action === \"build\" ? \"build\" : \"update\";\n // On CI a preview update still ships its tag; this machine would ship code no release named.\n const named = movedPast ? await codeSince(analysis.git.head) : [];\n const notLocal = movedPast\n ? {\n notLocal: `main moved past it (${named.slice(0, 3).join(\", \")}${named.length > 3 ? \", ...\" : \"\"} changed since)`,\n }\n : {};\n switch (movedPast && kind === \"update\" ? kind : ship) {\n case \"build\": {\n const refusal = await buildRefusal(context, analysis, input);\n return refusal\n ? refuse(refusal)\n : { action: \"ship\", kind: \"build\", jobs: [buildJob(input)], ...notLocal };\n }\n case \"update\":\n return { action: \"ship\", kind: \"update\", jobs: [updateJob(input)], ...notLocal };\n case \"wait\":\n return {\n action: \"none\",\n status: `${unit.name} ${version} is building on EAS, submitted when done`,\n };\n case \"shipped\":\n return { action: \"none\", status: `${unit.name} ${version} is on preview` };\n case \"nothing\":\n return {\n action: \"none\",\n status: `No ${unit.name} version to ship: add a changeset, then release.`,\n };\n case \"behind\":\n return refuse({\n title: \"main is behind\",\n detail: \"the last preview build carries a newer version.\",\n commands: [\"git pull\"],\n });\n case \"moved-past\": {\n const changed = await codeSince(analysis.git.head);\n const named = changed.slice(0, 3).join(\", \") + (changed.length > 3 ? \", ...\" : \"\");\n return refuse({\n title: `main moved past ${tagName(unit.name, version, context.tagFormat)}`,\n detail: `a binary or an update ships its commit, and ${named} changed since, code no release named.`,\n commands: [\"pnpm changeset\", \"shiplane\"],\n });\n }\n }\n};\n\nconst productionDecision = async (\n context: DecideContext,\n input: JobInput<Options>,\n): Promise<Decision> => {\n const { unit, options } = input;\n const client = easClient(context.shell, path.join(context.root, unit.dir));\n const productionVersion = binaryVersion(context.root, unit, options, \"production\");\n const analysis = await analyse(\n client,\n context.sourceControl,\n \"production\",\n productionVersion,\n unit.name,\n );\n const version = unit.version;\n const tag = await context.sourceControl.tagCommit(tagName(unit.name, version, context.tagFormat));\n const codeSince = (commit: string) => (tag ? context.changedCode(unit, tag, commit) : []);\n const sinceTag = async (commit: string | null) =>\n tag !== null && commit !== null && (await codeSince(commit)).length === 0;\n const action = analysis.plan.action;\n const lastUpdate = tag && action === \"update\" ? await client.lastUpdate(\"production\") : null;\n const origin = lastUpdate ? await client.updateOrigin(lastUpdate.group, \"production\") : null;\n const updateCommit = origin?.commit ?? null;\n const shippedCommits = [\n ...analysis.checked.map(({ state }) => state.shipped?.gitCommit ?? null),\n updateCommit,\n ];\n const builtVersion = analysis.checked[0]?.state.shipped?.appVersion ?? null;\n const shippedAny = (await Promise.all(shippedCommits.map(sinceTag))).some(Boolean);\n const rollout = lastUpdate?.rolloutPercentage ?? 100;\n const decision = productionMobile(action, {\n tagged: tag !== null,\n sinceTagOnlyVersionFiles: await sinceTag(analysis.git.head),\n expoVersion: version,\n productionVersion,\n codeShipped: action === \"submit\" || shippedAny,\n builtVersion,\n partialRollout: rollout < 100 && (await sinceTag(updateCommit)),\n });\n switch (decision) {\n case \"finish-rollout\":\n return lastUpdate\n ? rolloutChoice(context, client, input, {\n group: lastUpdate.group,\n percentage: rollout,\n createdAt: origin?.createdAt ?? null,\n })\n : { action: \"none\", status: `${unit.name} ${version} is live` };\n case \"promote\":\n return {\n action: \"pull-request\",\n question: `Open the PR that moves production to ${unit.name} ${version}?`,\n branch: `chore/promote-${unit.name}-${version}`,\n title: `chore(release): production builds ${unit.name} ${version}`,\n body: [\n \"## ๐ŸŽฏ What & why\",\n \"\",\n `Production builds ${unit.name} ${version}, the version preview validated: \\`${path.posix.join(unit.dir, options.versionFile)}\\` production moves to it, the runtime version production updates target. Merge it, then \\`release --channel production\\` builds.`,\n \"\",\n \"## โœ… Testing\",\n \"\",\n \"Version file only. No UI change.\",\n ].join(\"\\n\"),\n changesets: [],\n write: () =>\n Promise.resolve([setBinaryVersion(context.root, unit, options, \"production\", version)]),\n };\n case \"update\":\n return { action: \"ship\", kind: \"update\", jobs: [updateJob(input)] };\n case \"build\": {\n const refusal = await buildRefusal(context, analysis, input);\n return refusal ? refuse(refusal) : { action: \"ship\", kind: \"build\", jobs: [buildJob(input)] };\n }\n case \"wait\":\n return {\n action: \"none\",\n status: `${unit.name} ${productionVersion} is building on EAS, submitted to the stores when done`,\n };\n case \"shipped\":\n return {\n action: \"none\",\n status: `${unit.name} ${builtVersion ?? version} is live`,\n };\n case \"nothing\":\n return {\n action: \"none\",\n status: `No ${unit.name} release to promote yet: release to preview first.`,\n };\n case \"behind\":\n return refuse({\n title: \"main is behind\",\n detail: \"the last production build carries a newer version.\",\n commands: [\"git pull\"],\n });\n case \"moved-past\": {\n const changed = await codeSince(analysis.git.head);\n const named = changed.slice(0, 3).join(\", \") + (changed.length > 3 ? \", ...\" : \"\");\n return refuse({\n title: `main moved past ${tagName(unit.name, version, context.tagFormat)}`,\n detail: `production ships what preview validated, and ${named} changed since, code preview never ran: release it to preview first.`,\n commands: [\"pnpm changeset\", \"shiplane\"],\n });\n }\n }\n};\n\nconst ON_MERGE: Partial<Record<MobileRelease[\"ships\"], string>> = {\n build: \"Build iOS and Android, submitted when done\",\n update: \"Over-the-air update\",\n};\n\n/**\n * A mobile app on EAS. The native fingerprint decides between a new binary and an over-the-air\n * update; production ships what preview validated, moving its version by PR first when it must.\n */\nexport type EasMobileDistributionAdapter = DeliveryAdapter<MobileDistributionPort>;\n\nexport const easMobileDistribution = (): EasMobileDistributionAdapter =>\n defineMobileDistribution({\n name: \"eas\",\n settings: EasOptions,\n decidedBy: { platform: \"EAS\", choice: \"build or update\", check: \"shiplane status --json\" },\n typicalMinutes: 8,\n // The binary versions move after a tag; the code does not.\n versionFiles: (unit) => [path.posix.join(unit.dir, EasOptions.parse(unit.options).versionFile)],\n describe: ({ environment }) => `EAS ${environment}, iOS and Android`,\n detect: (folder) => {\n if (!folder.has(\"eas.json\")) return null;\n const scripts = z\n .object({ scripts: z.record(z.string(), z.string()).default({}) })\n .safeParse(JSON.parse(folder.read(\"package.json\") ?? \"{}\"));\n const names = scripts.success ? Object.keys(scripts.data.scripts) : [];\n const pick = (environment: string) =>\n [`eas:update:${environment}`, `eas:update:${environment}:gated`].find((name) =>\n names.includes(name),\n ) ?? names.find((name) => name.includes(\"update\") && name.includes(environment));\n const [preview, production] = [pick(\"preview\"), pick(\"production\")];\n return {\n certainty: \"found\",\n reason: \"eas.json\",\n options: preview && production ? { update: { preview, production } } : {},\n };\n },\n questions: (_folder, found) =>\n found.update === undefined\n ? [\n {\n key: \"update\",\n message: \"Package script that publishes an update?\",\n initial: \"eas:update\",\n },\n ]\n : [],\n requirements: (unit) => [\n {\n label: \"eas\",\n check: { command: \"pnpm\", args: [\"exec\", \"eas\", \"whoami\"], cwd: unit.dir },\n fix: { command: \"pnpm\", args: [\"exec\", \"eas\", \"login\"], cwd: unit.dir },\n },\n ],\n jobs: (input) => [buildJob(input), updateJob(input)],\n decide: (context, input) =>\n input.environment === \"preview\"\n ? previewDecision(context, input)\n : productionDecision(context, input),\n inspect: async (context, input) => {\n const { unit, environment, options } = input;\n const client = easClient(context.shell, path.join(context.root, unit.dir));\n const [analysis, lastUpdate] = await Promise.all([\n analyse(\n client,\n context.sourceControl,\n environment,\n binaryVersion(context.root, unit, options, environment),\n unit.name,\n ),\n client.lastUpdate(environment),\n ]);\n const rollout = lastUpdate?.rolloutPercentage ?? 100;\n return {\n ...analysisJson(analysis),\n ...(rollout < 100 ? { rolloutPercentage: rollout } : {}),\n };\n },\n // Only an update has launch data: a binary preview got for this release reports nothing here.\n health: async (context, { unit, tag }) => {\n const client = easClient(context.shell, path.join(context.root, unit.dir));\n const [last, tagged] = await Promise.all([\n client.lastUpdate(\"preview\"),\n context.sourceControl.tagCommit(tag),\n ]);\n if (!last || !tagged) return null;\n const { commit } = await client.updateOrigin(last.group, \"preview\");\n if (!commit || (await context.changedCode(unit, tagged, commit)).length > 0) return null;\n return updateHealth(await client.updateLaunches(last.group, \"preview\"));\n },\n beforeVersion: async (context, input) => {\n const { unit, options, bump } = input;\n const client = easClient(context.shell, path.join(context.root, unit.dir));\n const analysis = await analyse(\n client,\n context.sourceControl,\n \"preview\",\n binaryVersion(context.root, unit, options, \"preview\"),\n unit.name,\n );\n const forced = input.chosen === \"build\" && analysis.plan.action !== \"build\";\n const mobile = mobileRelease(forced ? \"build\" : analysis.plan.action, bump, unit.version);\n const summary = forced\n ? [\"โ†‘ build asked for, an update would do\", ...verdictLines(analysis)]\n : verdictLines(analysis);\n if (mobile.ships === \"blocked\") {\n return {\n blocked: {\n title: \"No release\",\n detail:\n mobile.why === \"build-running\"\n ? \"a preview build is still running: release once it's done, the native layer it carries decides the version.\"\n : \"main is behind the last preview build's version.\",\n commands: [\"shiplane\"],\n },\n added: [],\n writes: [],\n summary,\n onMerge: \"Nothing ships\",\n };\n }\n const changes = [...new Set(analysis.checked.flatMap(({ changes: lines }) => lines))];\n const version =\n mobile.ships === \"build\" || mobile.ships === \"update\" ? mobile.version : unit.version;\n return {\n choice: mobileChoice(analysis.plan.action, planWhy(analysis), unit.name),\n added: releaseChangesets(mobile, changes, unit.packageName),\n writes:\n mobile.ships === \"build\"\n ? [\n {\n label: `preview app version โ†’ ${version}`,\n run: async () => {\n setBinaryVersion(context.root, unit, options, \"preview\", version);\n await Promise.resolve();\n },\n },\n ]\n : [],\n summary: mobile.ships === \"nothing\" ? [\"โ€“ nothing changed\"] : summary,\n onMerge: ON_MERGE[mobile.ships] ?? \"Nothing ships\",\n ...(mobile.ships === \"build\"\n ? {\n step: `Build iOS and Android ${version} on EAS, submitted to TestFlight and Play internal when done`,\n }\n : mobile.ships === \"update\"\n ? { step: `Publish ${version} over the air to preview` }\n : {}),\n };\n },\n });\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,54 @@
1
1
  {
2
2
  "name": "@shiplane/eas",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "Mobile distribution for shiplane: an Expo app on EAS, a store build or an over-the-air update.",
5
+ "homepage": "https://github.com/arnaud-zg/packages/tree/main/packages/shiplane-eas#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/arnaud-zg/packages/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/arnaud-zg/packages.git",
12
+ "directory": "packages/shiplane-eas"
13
+ },
14
+ "license": "MIT",
15
+ "author": "Arnaud Zheng <arnaud.zg@gmail.com>",
16
+ "sideEffects": false,
17
+ "type": "module",
18
+ "exports": {
19
+ ".": "./dist/index.js",
20
+ "./package.json": "./package.json"
21
+ },
22
+ "main": "./dist/index.js",
23
+ "module": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "devDependencies": {
29
+ "@arnaud-zg/configs": "0.3.0",
30
+ "@types/node": "^22.0.0",
31
+ "eslint": "^9.39.4",
32
+ "prettier": "^3.8.3",
33
+ "typescript": "6.0.3",
34
+ "vitest": "^4.1.11",
35
+ "@shiplane/fly": "0.1.1",
36
+ "shiplane": "0.2.0"
37
+ },
38
+ "peerDependencies": {
39
+ "shiplane": "0.2.0"
40
+ },
41
+ "shiplane": {
42
+ "adapters": [
43
+ "easMobileDistribution"
44
+ ]
45
+ },
46
+ "scripts": {
47
+ "build": "tsdown",
48
+ "clean": "git clean -xdf .cache .turbo dist node_modules",
49
+ "format": "prettier --write . --cache --cache-location .cache/.prettiercache",
50
+ "lint": "eslint --cache --cache-location .cache/.eslintcache",
51
+ "test": "vitest run",
52
+ "typecheck": "tsc -p tsconfig.typecheck.json"
53
+ }
6
54
  }