fastlane-plugin-pharen 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8cd4f080adf3fef449642139ef7ca42ee6f186325ca8948ee05dc7fdab939c3a
4
- data.tar.gz: 4db24993e8d906973ab4c91e8df26d26bd01104c00c332c3d2419a85b62f4b54
3
+ metadata.gz: dadecec8055f7967c935f9a8679dfd48f489f848f7ad7477bff1e1d6b87a4158
4
+ data.tar.gz: 4b0a0db3a15232e961f545a7264b94c89bc329c8ea25db487c0f575cffd93323
5
5
  SHA512:
6
- metadata.gz: a05b26ce9c1de6c6c1fa251948a7d6af63bd6e89bf4bef1338d7247c6ca37290e7dbbbe5864ab2d83f51450ed719b23ebdd2d6d993e9e0a4140e7891af2137c2
7
- data.tar.gz: 3b6f71ab17e05f1b1f50488360a7533e2dbfe5757d0b36f536c42daef9f662f092be6a628f2b9e94bee3d4de2b9a4ff6671d6cd9dff164ad095e7253ea0b8f9a
6
+ metadata.gz: b818317f1e4a81ea82160b707888e66db4ff15ae6f5882710112c4699460dee62e18d4b229916b37d307d6c56bf5b1f7434981fb6637b3c19ecffca157593042
7
+ data.tar.gz: b04d8bfc3b3e651938c00b5ad1dd4f0ddc9006de0f8b78f713b3f16c445d7af3c6f4b4f9a0fd93fed1da1614bbf4727db0cb7a95d9efd324bfaf3062e7046251
data/CHANGELOG.md CHANGED
@@ -2,6 +2,44 @@
2
2
 
3
3
  All notable changes to `fastlane-plugin-pharen` are documented here.
4
4
 
5
+ ## 0.3.0
6
+
7
+ - Requires `@pharen-ai/cli` 0.4.0 or later. The plugin requests JSON explicitly and
8
+ works with CLI releases that print readable text by default.
9
+
10
+ ## 0.2.0
11
+
12
+ - **Requires `@pharen-ai/cli` 0.1.0 or later** (`npm install -g @pharen-ai/cli`). The
13
+ actions call it by its current commands — `releases create`, `symbols upload`,
14
+ `builds upload`. 0.1.0 called them by older names the CLI does not accept, and because
15
+ 0.1.0 warned rather than failed, a lane on it would skip every step without uploading
16
+ anything.
17
+ - **`strict` defaults to `true`.** A step that fails now fails the lane; pass
18
+ `strict: false` (or `PHAREN_STRICT=false`) to warn and continue as 0.1.0 did. A build
19
+ shipped without its symbols produces crashes that stay unreadable until they arrive,
20
+ which a warning in a long lane log too easily hides.
21
+ - **In strict mode, `pharen_release` checks its dSYMs before it registers the release.**
22
+ When none are found, a dSYM path does not exist, or a zip holds no `.dSYM`, the lane
23
+ fails before anything is sent; with `strict: true`, 0.1.0 registered the release first and
24
+ then failed at the symbols step, leaving a release with no build. A dSYM the CLI or the
25
+ server rejects at upload still fails the lane after the release is registered. With `strict: false` it warns and uploads the build, as before.
26
+ - **`PHAREN_STRICT` takes `true` or `false`** (also `yes`/`no`, `on`/`off`, each in
27
+ lowercase or all caps). Any other value, `1`, `0` and an empty one included, now fails
28
+ the lane instead of being read as `true`; unset it to take the default.
29
+ - **`pharen_release`'s closing line says what happened** — release registered or not,
30
+ symbols uploaded or not — rather than claiming all three steps after one was skipped.
31
+ - **GitLab CI signs in once per command.** A GitLab ID token signs in once, so a job
32
+ declares `PHAREN_ID_TOKEN_1`, `_2`, ... in its `id_tokens:` block, one per `pharen`
33
+ command, and the plugin hands each command the next in that command's own environment.
34
+ The plugin takes the tokens out of the process environment when fastlane loads it, so no
35
+ other action or build step inherits one, a lone `PHAREN_ID_TOKEN` included. `pharen_release`
36
+ (three commands, two with no dSYMs and `strict: false`) checks the count before it runs anything and fails the lane when the job is short
37
+ or declares a single `PHAREN_ID_TOKEN`. An exported `PHAREN_AUTH_TOKEN` turns this off.
38
+ - **Credentials are the CLI's.** The CLI uses `PHAREN_AUTH_TOKEN` when it is exported, a
39
+ CI job's own identity once its pipeline is bound, or the session `pharen auth login`
40
+ stored; the plugin never reads, prints, or passes any of them, beyond handing a GitLab
41
+ job's ID tokens on one per command.
42
+
5
43
  ## 0.1.0 — initial release
6
44
 
7
45
  Three actions, a thin wrapper over the `pharen` CLI (auth, `.pharen.yml` resolution,
@@ -20,8 +58,8 @@ debug-id parsing and server-side dedupe all live there):
20
58
  **Failure posture:** warn-don't-fail by default — a telemetry step should never be the
21
59
  reason a release build fails. Pass `strict: true` (or set `PHAREN_STRICT=1`) in CI lanes
22
60
  where an unsymbolicated release must be a hard stop. A missing-dSYM situation always
23
- warns loudly regardless of mode, since a build shipped without symbols leaves every
24
- crash it produces unsymbolicated forever.
61
+ warns loudly regardless of mode, since a build shipped without symbols produces crashes
62
+ that stay unreadable until its symbols are uploaded.
25
63
 
26
64
  **Auth:** `PHAREN_AUTH_TOKEN` is read from the environment only — the plugin never
27
65
  reads, prints, or passes it on a command line.
data/README.md CHANGED
@@ -5,6 +5,10 @@ Ship an iOS build to Pharen from your Fastfile. One action, `pharen_release`, ru
5
5
  uploads the build, and returns an OTA install link — with nothing to configure in the
6
6
  lane.
7
7
 
8
+ [Continue with organization approval](https://console.pharen.ai/setup/application) to move beyond evaluation.
9
+ Approved organizations can use test and live keys and upload matching dSYMs for readable
10
+ crash stacks. Your paid plan determines which features and usage limits apply.
11
+
8
12
  The plugin is a thin wrapper over the `pharen` CLI. Authentication, `.pharen.yml`
9
13
  resolution, debug-id parsing, and server-side dedupe all live in the CLI; the plugin only
10
14
  derives what the CLI needs from the lane context and the built artifacts. It adds no
@@ -18,9 +22,10 @@ fastlane add_plugin pharen
18
22
 
19
23
  Once per repository, you also need:
20
24
 
21
- - the `pharen` CLI on `PATH` (or point `pharen_binary:` / `PHAREN_BINARY` at it),
25
+ - the `pharen` CLI (0.4.0 or later) on `PATH` — `npm install -g @pharen-ai/cli` (or point
26
+ `pharen_binary:` / `PHAREN_BINARY` at it),
22
27
  - a committed `.pharen.yml` at the repo root (`org:` and `app:` — no secrets),
23
- - `PHAREN_AUTH_TOKEN` in the environment (your one secret — see [Configuration](#configuration)).
28
+ - a credential the CLI can use — see [Configuration](#configuration).
24
29
 
25
30
  ## Quickstart
26
31
 
@@ -37,6 +42,17 @@ from git. It registers the release, uploads every dSYM the archive produced, upl
37
42
  build, prints the OTA install link, and returns it (also available in
38
43
  `lane_context[SharedValues::PHAREN_INSTALL_URL]`).
39
44
 
45
+ `pharen_release` is for tester builds: it always uploads the build for an install link. In
46
+ a TestFlight or App Store lane, upload the symbols before the build leaves:
47
+
48
+ ```ruby
49
+ lane :release do
50
+ build_app(scheme: "MyApp", export_method: "app-store")
51
+ pharen_upload_symbols
52
+ upload_to_testflight
53
+ end
54
+ ```
55
+
40
56
  ## Actions
41
57
 
42
58
  ### `pharen_release`
@@ -47,11 +63,12 @@ The whole integration in one call after `build_app`.
47
63
  |---|---|---|---|
48
64
  | `ipa` | `PHAREN_IPA` | lane context (`IPA_OUTPUT_PATH`) | Path to the `.ipa`. |
49
65
  | `dsym_paths` | `PHAREN_DSYM_PATHS` | lane context (`DSYM_OUTPUT_PATH` + `DSYM_PATHS`) | dSYM bundles or `.zip`s to upload. |
50
- | `pharen_binary` | `PHAREN_BINARY` | `pharen` | Command that invokes the CLI (may include arguments, e.g. `node /path/to/cli.js`). |
51
- | `strict` | `PHAREN_STRICT` | `false` | Fail the lane on any step's error instead of warning and continuing. |
66
+ | `pharen_binary` | `PHAREN_BINARY` | `pharen` | Command that invokes the CLI (may include arguments, e.g. `npx --yes @pharen-ai/cli`). |
67
+ | `strict` | `PHAREN_STRICT` | `true` | Fail the lane on any step's error. `false` warns and continues instead. |
52
68
 
53
- Returns the install link on success, or `nil` when a step is skipped or fails outside
54
- strict mode.
69
+ Returns the install link once the build uploads, even if registration or the symbols
70
+ step failed with `strict: false`. It returns `nil` when there is no IPA, its `Info.plist`
71
+ cannot be read, or the build upload fails (with `strict: false`).
55
72
 
56
73
  ### `pharen_upload_build`
57
74
 
@@ -65,7 +82,7 @@ pharen_upload_build # IPA from the lane context → install link
65
82
  |---|---|---|---|
66
83
  | `ipa` | `PHAREN_IPA` | lane context (`IPA_OUTPUT_PATH`) | Path to the `.ipa`. |
67
84
  | `pharen_binary` | `PHAREN_BINARY` | `pharen` | Command that invokes the CLI. |
68
- | `strict` | `PHAREN_STRICT` | `false` | Fail the lane on error instead of warning and continuing. |
85
+ | `strict` | `PHAREN_STRICT` | `true` | Fail the lane on error. `false` warns and continues instead. |
69
86
 
70
87
  ### `pharen_upload_symbols`
71
88
 
@@ -80,52 +97,93 @@ pharen_upload_symbols(dsym_paths: ["path/to/App.dSYM"]) # or explicit
80
97
  |---|---|---|---|
81
98
  | `dsym_paths` | `PHAREN_DSYM_PATHS` | lane context (`DSYM_OUTPUT_PATH` + `DSYM_PATHS`) | `.dSYM` bundles, dSYM `.zip`s (as produced by `gym`/`download_dsyms`), or raw DWARF binaries. |
82
99
  | `pharen_binary` | `PHAREN_BINARY` | `pharen` | Command that invokes the CLI. |
83
- | `strict` | `PHAREN_STRICT` | `false` | Fail the lane on error instead of warning and continuing. |
100
+ | `strict` | `PHAREN_STRICT` | `true` | Fail the lane on error. `false` warns and continues instead. |
84
101
 
85
102
  ## Configuration
86
103
 
87
- Two things live outside the lane, and only one of them is a secret:
104
+ Two things live outside the lane:
88
105
 
89
106
  - **`.pharen.yml`** — committed at the repo root, holding `org:` and `app:`. No secrets
90
107
  belong in it. The CLI walks up from its working directory to find it, so it resolves
91
108
  whether fastlane runs from the repo root or the `fastlane/` directory.
92
- - **`PHAREN_AUTH_TOKEN`** — your one secret, read from the environment (a CI secret in
93
- practice). Only the CLI reads it, and only from the environment. The plugin never
94
- reads, prints, or passes the token, so it never lands in a log line or a process
95
- argument list.
109
+ - **A credential** — the CLI finds its own, in this order: `PHAREN_AUTH_TOKEN` when it is
110
+ exported (a CI secret), then a CI job's own identity once an organization owner has
111
+ bound that pipeline (no secret at all), then the session `pharen auth login` stored on
112
+ your machine. The plugin never prints a credential or puts one in a process argument
113
+ list, and passes none on except a GitLab job's ID tokens, one per command (below). The [CLI reference](https://www.pharen.ai/docs/cli)
114
+ covers each.
115
+
116
+ ### GitLab CI
117
+
118
+ On GitHub Actions, each `pharen` command asks GitHub for a fresh ID token, so a bound job
119
+ needs nothing more. A GitLab ID token signs in once, and `pharen_release` runs three `pharen`
120
+ commands, so a GitLab job declares one token per command and the plugin hands each command
121
+ the next:
122
+
123
+ ```yaml
124
+ beta:
125
+ id_tokens:
126
+ PHAREN_ID_TOKEN_1: { aud: 'https://api.pharen.ai' }
127
+ PHAREN_ID_TOKEN_2: { aud: 'https://api.pharen.ai' }
128
+ PHAREN_ID_TOKEN_3: { aud: 'https://api.pharen.ai' }
129
+ script:
130
+ - bundle exec fastlane beta
131
+ ```
132
+
133
+ - **Count every action.** `pharen_release` spends three tokens (two when there are no dSYMs
134
+ to upload, or none could be prepared, and `strict` is false); `pharen_upload_build` and `pharen_upload_symbols` spend
135
+ one each. A job that runs a single one-command action may declare one `PHAREN_ID_TOKEN`
136
+ instead.
137
+ - **The tokens stay out of the lane.** The plugin reads them when fastlane loads it and
138
+ takes them out of the environment, a lone `PHAREN_ID_TOKEN` included, so no other action
139
+ or build step inherits one. Each
140
+ command gets its token in its own environment, and none is printed.
141
+ - **A short job fails up front.** `pharen_release` counts its tokens before it runs
142
+ anything, and fails the lane when the job holds too few or declares a single
143
+ `PHAREN_ID_TOKEN` instead, rather than registering the release and then being refused at
144
+ the upload. With `strict: false` it warns and skips all three steps.
145
+ - **`PHAREN_AUTH_TOKEN` wins.** When it is exported, the CLI uses it and the tokens go
146
+ unspent.
96
147
 
97
148
  ## Failure posture
98
149
 
99
- The default is **warn-don't-fail**: if a step errors, the plugin logs a loud warning and
100
- lets the build continue. A telemetry step should never be the reason a release build
101
- fails.
150
+ The default is **strict**: if a step errors, the lane fails. A build that ships without
151
+ its symbols produces crashes that stay unreadable until they arrive, so the plugin stops
152
+ rather than letting that pass unnoticed.
102
153
 
103
- Pass `strict: true` (or set `PHAREN_STRICT=1`) in CI lanes where an unsymbolicated or
104
- undistributed release must be a hard stop; then any step's error fails the lane.
154
+ Pass `strict: false` (or set `PHAREN_STRICT=false`) in lanes where a Pharen step must
155
+ never be the reason a build fails; then any step's error is logged as a loud warning and
156
+ the lane continues. `PHAREN_STRICT` takes `true` or `false` (also `yes`/`no`,
157
+ `on`/`off`, each in lowercase or all caps); any other value, an empty one included, fails
158
+ the lane rather than being guessed at. Unset it to take the default.
105
159
 
106
160
  Two things hold regardless of mode:
107
161
 
108
- - **Missing dSYMs always warn loudly.** A build shipped without its symbols produces
109
- crashes that can never be read, and uploading symbols later does not retroactively
110
- symbolicate the crashes it already caused. Archive with
111
- `DEBUG_INFORMATION_FORMAT=dwarf-with-dsym` so the dSYMs exist to upload.
162
+ - **Missing dSYMs are never quiet.** A build shipped without its symbols produces
163
+ crashes that stay unreadable until the symbols arrive. In strict mode, when no dSYM is
164
+ found, a dSYM path does not exist, or a zip holds no `.dSYM`, `pharen_release` stops
165
+ before it registers or uploads anything. Uploading them later
166
+ re-symbolicates and re-groups recent crashes, at most the last seven days of them;
167
+ older ones may stay as they first grouped. Archive with `DEBUG_INFORMATION_FORMAT=dwarf-with-dsym`
168
+ so the dSYMs exist to upload with the build.
112
169
  - **Re-runs are safe.** The server dedupes symbols by debug id, and the CLI exits 0 when
113
170
  every file is already uploaded — so retrying a lane never double-uploads or errors on
114
171
  work already done.
115
172
 
116
173
  ## Troubleshooting
117
174
 
118
- - **`pharen` not found.** The CLI isn't on `PATH`. Install it and make `pharen`
119
- resolvable, or point `pharen_binary:` / `PHAREN_BINARY` at it (it may include
120
- arguments, e.g. `node /path/to/cli.js`).
175
+ - **`pharen` not found.** The CLI isn't on `PATH`. Install it with
176
+ `npm install -g @pharen-ai/cli`, or point `pharen_binary:` / `PHAREN_BINARY` at a
177
+ command that runs it (it may include arguments, e.g. `npx --yes @pharen-ai/cli`).
121
178
  - **"no IPA" or "no dSYMs in the lane context".** Run the action after `build_app`/`gym`
122
179
  (and `download_dsyms`, if you fetch symbols from App Store Connect), or pass `ipa:` /
123
180
  `dsym_paths:` explicitly.
124
- - **Release registration warns but the build still uploads.** Expected in warn mode:
125
- registration records provenance, so a failed registration degrades metadata without
126
- stopping distribution. Use `strict: true` if you need it to stop the lane.
181
+ - **Release registration warns but the build still uploads.** Expected with
182
+ `strict: false`: registration records provenance (the commit), so a failed registration
183
+ loses that metadata without stopping distribution. Leave `strict` at its default if you
184
+ need it to stop the lane.
127
185
  - **Not using fastlane?** The `pharen` CLI runs the same three steps directly. See the
128
- documentation at <https://pharen.ai/docs>.
186
+ [CLI reference](https://www.pharen.ai/docs/cli).
129
187
 
130
188
  ## License
131
189
 
@@ -8,15 +8,16 @@
8
8
  #
9
9
  # Zero-arg: the IPA and dSYMs default from the lane context (gym/download_dsyms);
10
10
  # version / build number / bundle id are read from the IPA's own Info.plist; the
11
- # commit sha from git; org/app from the committed .pharen.yml; auth from
12
- # PHAREN_AUTH_TOKEN (env-only, never echoed). It folds Pharen's three CLI steps
13
- # (releases new → upload-symbols → upload-build) into one, so registering a release
11
+ # commit sha from git; org/app from the committed .pharen.yml; auth from the CLI's own
12
+ # credential (never passed or echoed). It folds Pharen's three CLI steps
13
+ # (releases create → symbols upload → builds upload) into one, so registering a release
14
14
  # is never a step you have to remember to run.
15
15
  #
16
- # SYMBOLS ARE NOT OPTIONAL: a build shipped without its dSYMs leaves every crash it
17
- # ever produces unsymbolicated (late upload does not retroactively fix issues). In
18
- # warn mode a missing-dSYM situation is a LOUD warning and the build still uploads;
19
- # strict mode aborts the lane.
16
+ # SYMBOLS ARE NOT OPTIONAL: a build shipped without its dSYMs produces crashes that stay
17
+ # unreadable until the dSYMs arrive (a late upload re-symbolicates and re-groups recent
18
+ # crashes only, at most the last seven days of them). Strict mode, the default, aborts
19
+ # the lane when no dSYM is found; with strict: false it is a LOUD warning and the build
20
+ # still uploads.
20
21
 
21
22
  require "fastlane/action"
22
23
  require_relative "../helper/pharen_helper"
@@ -46,19 +47,6 @@ module Fastlane
46
47
  return nil
47
48
  end
48
49
 
49
- # -- step 1: register the release (idempotent server-side; commit = provenance)
50
- release_args = ["releases", "new", "--platform", "ios", "--version", info[:version], "--build", info[:build]]
51
- sha = helper.git_commit_sha
52
- release_args += ["--commit", sha] if sha
53
- release = helper.run_pharen(params[:pharen_binary], release_args)
54
- unless release[:ok]
55
- # Not fatal in warn mode: ingest also binds releases lazily, so a failed
56
- # registration degrades provenance, not the pipeline.
57
- helper.fail_or_warn(params[:strict], helper.cli_failure_message("pharen releases new", release))
58
- return nil if params[:strict]
59
- end
60
-
61
- # -- step 2: symbols (every dSYM the lane produced) ---------------------------
62
50
  dsym_inputs = params[:dsym_paths]
63
51
  if dsym_inputs.nil? || Array(dsym_inputs).empty?
64
52
  candidates = []
@@ -66,31 +54,72 @@ module Fastlane
66
54
  candidates.concat(Array(Actions.lane_context[SharedValues::DSYM_PATHS]))
67
55
  dsym_inputs = candidates.compact.uniq
68
56
  end
57
+
58
+ # The dSYMs are found and prepared before anything runs, so that in strict mode a
59
+ # lane with none, or with one that cannot be read, stops before the release is
60
+ # registered rather than leaving a release with no build. With strict: false the
61
+ # problem is a loud warning at the symbols step, and the build still uploads.
62
+ dsyms = nil
63
+ dsym_problem = nil
69
64
  if Array(dsym_inputs).empty?
70
- helper.fail_or_warn(
71
- params[:strict],
65
+ dsym_problem =
72
66
  "pharen_release: no dSYMs in the lane context — crashes from this build " \
73
- "can NEVER symbolicate. Archive with DEBUG_INFORMATION_FORMAT=dwarf-with-dsym, " \
74
- "or pass dsym_paths:"
75
- )
67
+ "stay unreadable until its dSYMs are uploaded. Archive with " \
68
+ "DEBUG_INFORMATION_FORMAT=dwarf-with-dsym, or pass dsym_paths:"
76
69
  else
77
70
  begin
78
71
  dsyms = helper.expand_dsym_inputs(dsym_inputs)
79
- symbols = helper.run_pharen(params[:pharen_binary], ["upload-symbols", *dsyms])
80
- if symbols[:ok]
81
- summary = symbols[:json].is_a?(Hash) ? symbols[:json]["summary"] : nil
82
- UI.success("pharen_release: symbols — #{summary || "uploaded #{dsyms.length} dSYM(s)"}")
83
- else
84
- helper.fail_or_warn(params[:strict], helper.cli_failure_message("pharen upload-symbols", symbols))
85
- end
86
72
  rescue Helper::PharenHelper::Failure => e
87
- helper.fail_or_warn(params[:strict], "pharen_release: #{e.message}")
73
+ dsym_problem = "pharen_release: #{e.message}"
74
+ end
75
+ end
76
+ if dsym_problem && params[:strict]
77
+ helper.fail_or_warn(params[:strict], dsym_problem)
78
+ return nil
79
+ end
80
+
81
+ # A GitLab job needs one ID token per command, counted before the first one runs:
82
+ # a job short of tokens would otherwise register the release and then be refused
83
+ # at the upload.
84
+ commands = dsyms ? 3 : 2
85
+ shortfall = helper.id_token_shortfall("pharen_release", commands)
86
+ if shortfall
87
+ helper.fail_or_warn(params[:strict], shortfall)
88
+ return nil
89
+ end
90
+
91
+ # -- step 1: register the release (idempotent server-side; commit = provenance)
92
+ release_args = ["releases", "create", "--platform", "ios", "--version", info[:version], "--build", info[:build]]
93
+ sha = helper.git_commit_sha
94
+ release_args += ["--commit", sha] if sha
95
+ release = helper.run_pharen(params[:pharen_binary], release_args)
96
+ unless release[:ok]
97
+ # Not fatal in warn mode: an uploaded build or an arriving crash records its
98
+ # release anyway, so a failed registration loses provenance (the commit), not
99
+ # the release.
100
+ helper.fail_or_warn(params[:strict], helper.cli_failure_message("pharen releases create", release))
101
+ return nil if params[:strict]
102
+ end
103
+
104
+ # -- step 2: symbols (every dSYM the lane produced) ---------------------------
105
+ symbols_ok = false
106
+ if dsym_problem
107
+ helper.fail_or_warn(params[:strict], dsym_problem)
108
+ else
109
+ symbols = helper.run_pharen(params[:pharen_binary], ["symbols", "upload", *dsyms])
110
+ if symbols[:ok]
111
+ symbols_ok = true
112
+ summary = symbols[:json].is_a?(Hash) ? symbols[:json]["summary"] : nil
113
+ UI.success("pharen_release: symbols — #{summary || "uploaded #{dsyms.length} dSYM(s)"}")
114
+ else
115
+ helper.fail_or_warn(params[:strict], helper.cli_failure_message("pharen symbols upload", symbols))
88
116
  end
89
117
  end
90
118
 
91
119
  # -- step 3: the build itself → install link ----------------------------------
92
120
  build_args = [
93
- "upload-build",
121
+ "builds",
122
+ "upload",
94
123
  "--platform", "ios",
95
124
  "--version", info[:version],
96
125
  "--build", info[:build],
@@ -99,13 +128,20 @@ module Fastlane
99
128
  ]
100
129
  build = helper.run_pharen(params[:pharen_binary], build_args)
101
130
  unless build[:ok]
102
- helper.fail_or_warn(params[:strict], helper.cli_failure_message("pharen upload-build", build))
131
+ helper.fail_or_warn(params[:strict], helper.cli_failure_message("pharen builds upload", build))
103
132
  return nil
104
133
  end
105
134
 
106
135
  install_url = build[:json].is_a?(Hash) ? build[:json]["install_url"] : nil
107
136
  Actions.lane_context[SharedValues::PHAREN_INSTALL_URL] = install_url if install_url
108
- UI.success("pharen_release: #{info[:bundle_id]} #{info[:version]} (#{info[:build]}) registered + symbolicated + distributed")
137
+ # Says what actually happened: outside strict mode a step can fail and the lane
138
+ # still reaches here, and a line claiming all three would hide it.
139
+ outcome = [
140
+ release[:ok] ? "release registered" : "release NOT registered",
141
+ symbols_ok ? "symbols uploaded" : "symbols NOT uploaded",
142
+ "build uploaded"
143
+ ]
144
+ UI.success("pharen_release: #{info[:bundle_id]} #{info[:version]} (#{info[:build]}) — #{outcome.join(', ')}")
109
145
  UI.important("Install (Safari on the phone): #{install_url}") if install_url
110
146
  install_url
111
147
  end
@@ -119,16 +155,16 @@ module Fastlane
119
155
  end
120
156
 
121
157
  def self.authors
122
- ["Pharen (Lucubra LLC)"]
158
+ ["Pharen"]
123
159
  end
124
160
 
125
161
  def self.details
126
- "Orchestrates the Pharen CLI's three steps (releases new → upload-symbols → " \
127
- "upload-build) with everything derived: IPA + dSYMs from the lane context, " \
162
+ "Runs the Pharen CLI's three steps (releases create → symbols upload → " \
163
+ "builds upload) with everything derived: IPA + dSYMs from the lane context, " \
128
164
  "version/build/bundle id from the IPA's Info.plist, commit sha from git, " \
129
- "org/app from the committed .pharen.yml, auth from PHAREN_AUTH_TOKEN. " \
130
- "Missing dSYMs warn LOUDLY (or abort under strict) — a build without " \
131
- "symbols ships crashes that can never be read."
165
+ "org/app from the committed .pharen.yml, auth from the CLI's own credential. " \
166
+ "Missing dSYMs abort the lane (or warn LOUDLY with strict: false) — a build " \
167
+ "without symbols ships crashes that stay unreadable until its symbols are uploaded."
132
168
  end
133
169
 
134
170
  def self.available_options
@@ -145,27 +181,30 @@ module Fastlane
145
181
  is_string: false),
146
182
  FastlaneCore::ConfigItem.new(key: :pharen_binary,
147
183
  env_name: "PHAREN_BINARY",
148
- description: "Command that invokes the Pharen CLI (may include arguments, e.g. `node /path/to/cli.js`)",
184
+ description: "Command that invokes the Pharen CLI (may include arguments, e.g. `npx --yes @pharen-ai/cli`)",
149
185
  optional: true,
150
186
  default_value: "pharen",
151
187
  type: String),
152
188
  FastlaneCore::ConfigItem.new(key: :strict,
153
189
  env_name: "PHAREN_STRICT",
154
- description: "Fail the lane on any step's error (true) instead of warning and continuing (false)",
190
+ description: "Fail the lane on any step's error (true, default) instead of warning and " \
191
+ "continuing. Silent continuation ships builds whose symbols never arrived; " \
192
+ "pass strict: false only where that is an acceptable trade",
155
193
  optional: true,
156
- default_value: false,
157
- is_string: false)
194
+ default_value: true,
195
+ type: Fastlane::Boolean)
158
196
  ]
159
197
  end
160
198
 
161
199
  def self.output
162
200
  [
163
- ["PHAREN_INSTALL_URL", "The itms-services install link answered by the Pharen control plane"]
201
+ ["PHAREN_INSTALL_URL", "The itms-services install link Pharen returned for the build"]
164
202
  ]
165
203
  end
166
204
 
167
205
  def self.return_value
168
- "The install_url string on success; nil on skipped/failed-but-not-strict"
206
+ "The install_url string once the build uploads; nil when there is no IPA, its Info.plist " \
207
+ "cannot be read, or the build upload fails (with strict: false)"
169
208
  end
170
209
 
171
210
  def self.is_supported?(platform)
@@ -1,10 +1,9 @@
1
- # pharen_upload_build — upload the IPA to Pharen's dl distribution and get the
2
- # install link back.
1
+ # pharen_upload_build — upload the IPA to Pharen and get the install link back.
3
2
  #
4
3
  # Zero-arg capable: the IPA defaults from gym/build_app's SharedValues::IPA_OUTPUT_PATH,
5
4
  # and version / build number / bundle id are read out of the IPA's own Info.plist
6
5
  # (unzip + plutil — dependency-free), so the customer passes nothing. org/app come from
7
- # the committed .pharen.yml; auth from PHAREN_AUTH_TOKEN (env-only, never echoed).
6
+ # the committed .pharen.yml; auth from the CLI's own credential (never passed or echoed).
8
7
 
9
8
  require "fastlane/action"
10
9
  require_relative "../helper/pharen_helper"
@@ -37,7 +36,8 @@ module Fastlane
37
36
  end
38
37
 
39
38
  args = [
40
- "upload-build",
39
+ "builds",
40
+ "upload",
41
41
  "--platform", "ios",
42
42
  "--version", info[:version],
43
43
  "--build", info[:build],
@@ -46,7 +46,7 @@ module Fastlane
46
46
  ]
47
47
  result = helper.run_pharen(params[:pharen_binary], args)
48
48
  unless result[:ok]
49
- helper.fail_or_warn(params[:strict], helper.cli_failure_message("pharen upload-build", result))
49
+ helper.fail_or_warn(params[:strict], helper.cli_failure_message("pharen builds upload", result))
50
50
  return nil
51
51
  end
52
52
 
@@ -62,18 +62,17 @@ module Fastlane
62
62
  #####################################################
63
63
 
64
64
  def self.description
65
- "Upload the IPA to Pharen dl distribution and return the OTA install link (zero-arg after gym)"
65
+ "Upload the IPA to Pharen and return the OTA install link (zero-arg after gym)"
66
66
  end
67
67
 
68
68
  def self.authors
69
- ["Pharen (Lucubra LLC)"]
69
+ ["Pharen"]
70
70
  end
71
71
 
72
72
  def self.details
73
- "Thin wrapper over `pharen upload-build`: the IPA defaults from " \
73
+ "Thin wrapper over `pharen builds upload`: the IPA defaults from " \
74
74
  "SharedValues::IPA_OUTPUT_PATH; version, build number and bundle id are " \
75
- "extracted from the IPA's Info.plist so nothing needs passing. The server " \
76
- "owns the dl layout, opaque slug and manifest.plist; the returned " \
75
+ "extracted from the IPA's Info.plist so nothing needs passing. The returned " \
77
76
  "install_url is this action's return value and lands in " \
78
77
  "lane_context[SharedValues::PHAREN_INSTALL_URL]."
79
78
  end
@@ -87,22 +86,22 @@ module Fastlane
87
86
  type: String),
88
87
  FastlaneCore::ConfigItem.new(key: :pharen_binary,
89
88
  env_name: "PHAREN_BINARY",
90
- description: "Command that invokes the Pharen CLI (may include arguments, e.g. `node /path/to/cli.js`)",
89
+ description: "Command that invokes the Pharen CLI (may include arguments, e.g. `npx --yes @pharen-ai/cli`)",
91
90
  optional: true,
92
91
  default_value: "pharen",
93
92
  type: String),
94
93
  FastlaneCore::ConfigItem.new(key: :strict,
95
94
  env_name: "PHAREN_STRICT",
96
- description: "Fail the lane on error (true) instead of warning and continuing (false)",
95
+ description: "Fail the lane on error (true, default) instead of warning and continuing",
97
96
  optional: true,
98
- default_value: false,
99
- is_string: false)
97
+ default_value: true,
98
+ type: Fastlane::Boolean)
100
99
  ]
101
100
  end
102
101
 
103
102
  def self.output
104
103
  [
105
- ["PHAREN_INSTALL_URL", "The itms-services install link answered by the Pharen control plane"]
104
+ ["PHAREN_INSTALL_URL", "The itms-services install link Pharen returned for the build"]
106
105
  ]
107
106
  end
108
107
 
@@ -3,8 +3,8 @@
3
3
  # Zero-arg capable: with no options it takes the dSYMs the lane already produced —
4
4
  # gym/build_app's SharedValues::DSYM_OUTPUT_PATH and download_dsyms'
5
5
  # SharedValues::DSYM_PATHS. The CLI needs ONLY paths: debug ids are parsed from the
6
- # binaries, org/app come from the customer's committed .pharen.yml, auth from
7
- # PHAREN_AUTH_TOKEN in the environment (never echoed, never on a command line).
6
+ # binaries, org/app come from the customer's committed .pharen.yml, auth from the CLI's
7
+ # own credential (never echoed, never on a command line).
8
8
 
9
9
  require "fastlane/action"
10
10
  require_relative "../helper/pharen_helper"
@@ -33,9 +33,9 @@ module Fastlane
33
33
  return nil
34
34
  end
35
35
 
36
- result = helper.run_pharen(params[:pharen_binary], ["upload-symbols"] + expanded)
36
+ result = helper.run_pharen(params[:pharen_binary], ["symbols", "upload"] + expanded)
37
37
  unless result[:ok]
38
- helper.fail_or_warn(params[:strict], helper.cli_failure_message("pharen upload-symbols", result))
38
+ helper.fail_or_warn(params[:strict], helper.cli_failure_message("pharen symbols upload", result))
39
39
  return nil
40
40
  end
41
41
 
@@ -63,14 +63,14 @@ module Fastlane
63
63
  end
64
64
 
65
65
  def self.authors
66
- ["Pharen (Lucubra LLC)"]
66
+ ["Pharen"]
67
67
  end
68
68
 
69
69
  def self.details
70
- "Thin wrapper over `pharen upload-symbols`: dSYM paths default from " \
70
+ "Thin wrapper over `pharen symbols upload`: dSYM paths default from " \
71
71
  "SharedValues::DSYM_OUTPUT_PATH (gym) and SharedValues::DSYM_PATHS " \
72
72
  "(download_dsyms); .zip inputs are expanded to .dSYM bundles. Debug ids, " \
73
- "org/app (.pharen.yml) and auth (PHAREN_AUTH_TOKEN) are the CLI's job. " \
73
+ "org/app (.pharen.yml) and auth are the CLI's job. " \
74
74
  "Server dedupes by debug id, so re-runs are safe."
75
75
  end
76
76
 
@@ -83,16 +83,18 @@ module Fastlane
83
83
  type: Array),
84
84
  FastlaneCore::ConfigItem.new(key: :pharen_binary,
85
85
  env_name: "PHAREN_BINARY",
86
- description: "Command that invokes the Pharen CLI (may include arguments, e.g. `node /path/to/cli.js`)",
86
+ description: "Command that invokes the Pharen CLI (may include arguments, e.g. `npx --yes @pharen-ai/cli`)",
87
87
  optional: true,
88
88
  default_value: "pharen",
89
89
  type: String),
90
90
  FastlaneCore::ConfigItem.new(key: :strict,
91
91
  env_name: "PHAREN_STRICT",
92
- description: "Fail the lane on error (true) instead of warning and continuing (false)",
92
+ description: "Fail the lane on error (true, default) instead of warning and continuing. " \
93
+ "A build shipped without its dSYMs produces crashes that stay unreadable until " \
94
+ "the symbols arrive — pass strict: false only where that is an acceptable trade",
93
95
  optional: true,
94
- default_value: false,
95
- is_string: false)
96
+ default_value: true,
97
+ type: Fastlane::Boolean)
96
98
  ]
97
99
  end
98
100
 
@@ -9,8 +9,14 @@
9
9
  # Dependency-free by design: macOS system tools only (unzip, plutil, git) — every box
10
10
  # that can produce an IPA has them — so the gem adds NO runtime dependencies.
11
11
  #
12
- # SECURITY: PHAREN_AUTH_TOKEN is read by the CLI from the environment. This helper never
13
- # reads, prints, or passes the token — no command line ever contains it.
12
+ # SECURITY: the CLI resolves its own credential — the session `pharen auth login` stored, a
13
+ # CI job's identity, or PHAREN_AUTH_TOKEN from the environment. This helper never reads,
14
+ # prints, or passes one, so no command line ever contains it. The one thing it hands on is
15
+ # a GitLab job's ID tokens (PHAREN_ID_TOKEN_1, _2, ...): a GitLab ID token signs in once,
16
+ # and a lane runs several pharen commands, so each command is given the next token in its
17
+ # own environment only. The tokens are read when the plugin loads and taken out of the
18
+ # process environment, so no other action or build step can spend one first, and none is
19
+ # ever printed.
14
20
 
15
21
  require "fastlane_core/ui/ui"
16
22
  require "json"
@@ -32,6 +38,9 @@ module Fastlane
32
38
  # frameworks) never match.
33
39
  INFO_PLIST_ENTRY = %r{\APayload/[^/]+\.app/Info\.plist\z}
34
40
 
41
+ # A GitLab job's numbered ID tokens, one per pharen command (see SECURITY above).
42
+ ID_TOKEN_ENV = /\APHAREN_ID_TOKEN_(\d+)\z/
43
+
35
44
  class << self
36
45
  # -- IPA introspection ---------------------------------------------------------
37
46
 
@@ -96,17 +105,81 @@ module Fastlane
96
105
  found
97
106
  end
98
107
 
108
+ # -- GitLab ID tokens ------------------------------------------------------------
109
+
110
+ # Reads PHAREN_ID_TOKEN_1, _2, ... once, in numeric order, and removes every one of
111
+ # them from the process environment, so actions and build steps that run later
112
+ # inherit none. A job that declares a single PHAREN_ID_TOKEN instead holds a pool of
113
+ # one, taken out the same way: it signs in one command. Called when the plugin loads;
114
+ # later calls return the same pool.
115
+ def load_id_tokens
116
+ return if @id_tokens
117
+
118
+ names = ENV.keys.grep(ID_TOKEN_ENV).sort_by { |name| name[ID_TOKEN_ENV, 1].to_i }
119
+ @id_tokens = names.map { |name| ENV.delete(name) }.reject { |value| value.to_s.empty? }
120
+ @id_tokens_spent = 0
121
+ lone = ENV.delete("PHAREN_ID_TOKEN")
122
+ @lone_id_token = @id_tokens.empty? && !lone.to_s.empty?
123
+ @id_tokens = [lone] if @lone_id_token
124
+ end
125
+
126
+ # Forgets the loaded tokens so the next load_id_tokens reads the environment again.
127
+ def reset_id_tokens
128
+ @id_tokens = nil
129
+ @id_tokens_spent = 0
130
+ @lone_id_token = false
131
+ end
132
+
133
+ # True when each pharen command signs in with its own GitLab ID token. An exported
134
+ # PHAREN_AUTH_TOKEN turns this off: the CLI prefers it over a CI identity.
135
+ def per_command_id_tokens?
136
+ load_id_tokens
137
+ return false if @id_tokens_spent.zero? && @id_tokens.empty?
138
+ ENV["PHAREN_AUTH_TOKEN"].to_s.empty?
139
+ end
140
+
141
+ # Checks, before an action runs anything, that a GitLab job holds a token for each
142
+ # of the `count` pharen commands it is about to run. Returns nil when it does, or
143
+ # when the job is not signing in with GitLab ID tokens; otherwise the message to
144
+ # refuse with.
145
+ def id_token_shortfall(action, count)
146
+ return nil unless per_command_id_tokens?
147
+ return nil if @id_tokens.length >= count
148
+
149
+ if @lone_id_token
150
+ "#{action}: PHAREN_ID_TOKEN is one GitLab ID token, and one token signs in once. This " \
151
+ "action runs #{count} pharen commands: declare PHAREN_ID_TOKEN_1 through " \
152
+ "PHAREN_ID_TOKEN_#{count} in the job's id_tokens: block instead"
153
+ else
154
+ "#{action}: this action runs #{count} pharen command(s) and a GitLab ID token signs in " \
155
+ "once, but #{@id_tokens.length} unspent token(s) remain. Declare PHAREN_ID_TOKEN_1 " \
156
+ "through PHAREN_ID_TOKEN_#{@id_tokens_spent + count} in the job's id_tokens: block"
157
+ end
158
+ end
159
+
99
160
  # -- Pharen CLI invocation -----------------------------------------------------
100
161
 
101
- # Run `<binary> <args...>` and interpret the CLI's contract: JSON on stdout,
162
+ # Run `<binary> --json <args...>` and interpret the CLI's contract: JSON on stdout,
102
163
  # exit 0 on success. `binary` is Shellwords-split so `pharen_binary` may carry a
103
- # command with arguments (e.g. "node /path/to/cli.js"). Never raises on CLI
104
- # failure — callers decide strict-vs-warn from the returned hash.
164
+ # command with arguments (e.g. "npx --yes @pharen-ai/cli"). Never raises on CLI
165
+ # failure — callers decide strict-vs-warn from the returned hash. On a GitLab job
166
+ # signing in with per-command ID tokens, the command is given the next one as
167
+ # PHAREN_ID_TOKEN in its own environment.
105
168
  def run_pharen(binary, args, chdir: nil)
106
- argv = Shellwords.split(binary.to_s) + args.map(&:to_s)
169
+ argv = Shellwords.split(binary.to_s) + ["--json"] + args.map(&:to_s)
107
170
  dir = chdir || project_dir
108
- UI.command(Shellwords.join(argv)) # never contains PHAREN_AUTH_TOKEN (env-only)
109
- out, err, status = capture(argv, chdir: dir)
171
+ env = {}
172
+ if per_command_id_tokens?
173
+ if @id_tokens.empty?
174
+ return { ok: false, status: nil, stdout: "", json: nil, no_id_token: true,
175
+ stderr: "no GitLab ID token left for this command — declare PHAREN_ID_TOKEN_1 " \
176
+ "through PHAREN_ID_TOKEN_#{@id_tokens_spent + 1} in the job's id_tokens: block" }
177
+ end
178
+ env["PHAREN_ID_TOKEN"] = @id_tokens.shift
179
+ @id_tokens_spent += 1
180
+ end
181
+ UI.command(Shellwords.join(argv)) # never contains a credential (the CLI resolves its own)
182
+ out, err, status = capture(argv, chdir: dir, env: env)
110
183
  json = parse_json(out)
111
184
  { ok: status == 0, status: status, stdout: out, stderr: err, json: json }
112
185
  rescue Errno::ENOENT => e
@@ -119,10 +192,16 @@ module Fastlane
119
192
  def cli_failure_message(step, result)
120
193
  error = result[:json].is_a?(Hash) ? result[:json]["error"] : nil
121
194
  if error.is_a?(Hash) && error["message"]
195
+ if error["code"] == "command.unknown" && error["message"].to_s.start_with?("unknown command '--json ")
196
+ return "#{step} failed: this plugin needs Pharen CLI 0.4.0 or later. Update the CLI on PATH or point pharen_binary at a newer command."
197
+ end
198
+
122
199
  "#{step} failed: #{error['message']} (#{error['code']})"
200
+ elsif result[:no_id_token]
201
+ "#{step} failed: #{result[:stderr]}"
123
202
  elsif result[:not_found]
124
- "#{step} failed: #{result[:stderr]} — install the Pharen CLI and ensure `pharen` is on PATH " \
125
- "(see https://pharen.ai/docs for install instructions), or set the pharen_binary option"
203
+ "#{step} failed: #{result[:stderr]} — install the Pharen CLI with " \
204
+ "`npm install -g @pharen-ai/cli` so `pharen` is on PATH, or set the pharen_binary option"
126
205
  else
127
206
  detail = result[:stderr].to_s.strip
128
207
  detail = result[:stdout].to_s.strip if detail.empty?
@@ -134,11 +213,8 @@ module Fastlane
134
213
  # Where to run the CLI so it finds the customer's committed .pharen.yml.
135
214
  # fastlane executes lanes from the fastlane/ directory (Dir.chdir in
136
215
  # LaneManager), but .pharen.yml lives at the repo root by convention — step up
137
- # one level when that is clearly the layout. Explicit --org/--app flags are the
138
- # escape hatch for exotic layouts.
139
- # (The CLI itself also walks up from its cwd looking for .pharen.yml, so this
140
- # special-case is now a belt-and-suspenders optimization, not the only path —
141
- # kept as-is since it still resolves correctly and needs no change.)
216
+ # one level when that is clearly the layout. Any other layout is left to the
217
+ # CLI, which walks up from its working directory to the nearest .pharen.yml.
142
218
  def project_dir
143
219
  cwd = Dir.pwd
144
220
  return cwd if File.exist?(File.join(cwd, ".pharen.yml"))
@@ -152,7 +228,7 @@ module Fastlane
152
228
 
153
229
  # -- misc ------------------------------------------------------------------------
154
230
 
155
- # HEAD sha for `releases new --commit`; nil outside a git checkout (fine — the
231
+ # HEAD sha for `releases create --commit`; nil outside a git checkout (fine — the
156
232
  # flag is optional provenance).
157
233
  def git_commit_sha
158
234
  out, _err, status = capture(%w[git rev-parse HEAD])
@@ -161,8 +237,8 @@ module Fastlane
161
237
  sha.empty? ? nil : sha
162
238
  end
163
239
 
164
- # strict: abort the lane; default: warn loudly and let the build continue —
165
- # a telemetry step should never be the reason a release build fails.
240
+ # strict (the default): abort the lane. strict: false: warn loudly and let the
241
+ # build continue, for a lane where a Pharen step must never fail the build.
166
242
  def fail_or_warn(strict, message)
167
243
  if strict
168
244
  UI.user_error!(message)
@@ -173,12 +249,13 @@ module Fastlane
173
249
 
174
250
  # -- plumbing (the single shell-out seam; specs stub exactly this) ---------------
175
251
 
176
- # argv -> [stdout, stderr, exitstatus]; binmode because plists are binary.
177
- def capture(argv, stdin: nil, chdir: nil)
252
+ # argv -> [stdout, stderr, exitstatus]; binmode because plists are binary. `env` is
253
+ # added to the child's environment only.
254
+ def capture(argv, stdin: nil, chdir: nil, env: {})
178
255
  options = { binmode: true }
179
256
  options[:stdin_data] = stdin unless stdin.nil?
180
257
  options[:chdir] = chdir unless chdir.nil?
181
- out, err, status = Open3.capture3(*argv, **options)
258
+ out, err, status = Open3.capture3(env, *argv, **options)
182
259
  [out, err, status.exitstatus]
183
260
  end
184
261
 
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module Pharen
3
- VERSION = "0.1.0".freeze
3
+ VERSION = "0.3.0".freeze
4
4
  end
5
5
  end
@@ -16,3 +16,7 @@ end
16
16
  Fastlane::Pharen.all_classes.each do |current|
17
17
  require current
18
18
  end
19
+
20
+ # A GitLab job's ID tokens leave the environment before any lane runs, so no other action
21
+ # or build step inherits one it could spend first.
22
+ Fastlane::Helper::PharenHelper.load_id_tokens
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-pharen
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
- - Pharen (Lucubra LLC)
7
+ - Pharen
8
8
  bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
@@ -51,12 +51,12 @@ dependencies:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0'
54
- description: 'Fastlane plugin for Pharen (https://pharen.ai): a thin wrapper over
55
- the `pharen` CLI that adds one zero-argument action, pharen_release, after build_app
56
- — it registers the release, uploads every dSYM, uploads the build, and returns the
57
- OTA install link. Warn-don''t-fail by default, so a telemetry step never fails your
58
- release build; `strict: true` opts a lane into hard failure where an unsymbolicated
59
- release must be a stop.'
54
+ description: 'Fastlane plugin for Pharen (https://www.pharen.ai): a thin wrapper over
55
+ the `pharen` CLI (npm install -g @pharen-ai/cli) that adds one zero-argument action,
56
+ pharen_release, after build_app — it registers the release, uploads every dSYM,
57
+ uploads the build, and returns the OTA install link. A step that fails fails the
58
+ lane by default, so a build never ships without its symbols unnoticed; `strict:
59
+ false` lets a lane warn and continue instead.'
60
60
  email: hello@pharen.ai
61
61
  executables: []
62
62
  extensions: []
@@ -71,11 +71,11 @@ files:
71
71
  - lib/fastlane/plugin/pharen/actions/pharen_upload_symbols_action.rb
72
72
  - lib/fastlane/plugin/pharen/helper/pharen_helper.rb
73
73
  - lib/fastlane/plugin/pharen/version.rb
74
- homepage: https://pharen.ai
74
+ homepage: https://www.pharen.ai/docs/fastlane
75
75
  licenses:
76
76
  - MIT
77
77
  metadata:
78
- homepage_uri: https://pharen.ai
78
+ homepage_uri: https://www.pharen.ai/docs/fastlane
79
79
  rubygems_mfa_required: 'true'
80
80
  rdoc_options: []
81
81
  require_paths:
@@ -91,7 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
91
  - !ruby/object:Gem::Version
92
92
  version: '0'
93
93
  requirements: []
94
- rubygems_version: 4.0.11
94
+ rubygems_version: 4.0.20
95
95
  specification_version: 4
96
96
  summary: 'Pharen build distribution + crash symbols from your lane: pharen_release
97
97
  / pharen_upload_build / pharen_upload_symbols'