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 +4 -4
- data/CHANGELOG.md +40 -2
- data/README.md +87 -29
- data/lib/fastlane/plugin/pharen/actions/pharen_release_action.rb +87 -48
- data/lib/fastlane/plugin/pharen/actions/pharen_upload_build_action.rb +14 -15
- data/lib/fastlane/plugin/pharen/actions/pharen_upload_symbols_action.rb +13 -11
- data/lib/fastlane/plugin/pharen/helper/pharen_helper.rb +98 -21
- data/lib/fastlane/plugin/pharen/version.rb +1 -1
- data/lib/fastlane/plugin/pharen.rb +4 -0
- metadata +11 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dadecec8055f7967c935f9a8679dfd48f489f848f7ad7477bff1e1d6b87a4158
|
|
4
|
+
data.tar.gz: 4b0a0db3a15232e961f545a7264b94c89bc329c8ea25db487c0f575cffd93323
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
24
|
-
|
|
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`
|
|
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
|
-
-
|
|
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. `
|
|
51
|
-
| `strict` | `PHAREN_STRICT` | `
|
|
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
|
|
54
|
-
strict
|
|
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` | `
|
|
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` | `
|
|
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
|
|
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
|
-
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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 **
|
|
100
|
-
|
|
101
|
-
|
|
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:
|
|
104
|
-
|
|
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
|
|
109
|
-
crashes that
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
119
|
-
|
|
120
|
-
arguments, e.g. `
|
|
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
|
|
125
|
-
registration records provenance, so a failed registration
|
|
126
|
-
stopping distribution.
|
|
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
|
-
|
|
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
|
-
#
|
|
13
|
-
# (releases
|
|
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
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# strict
|
|
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
|
-
|
|
71
|
-
params[:strict],
|
|
65
|
+
dsym_problem =
|
|
72
66
|
"pharen_release: no dSYMs in the lane context — crashes from this build " \
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
|
158
|
+
["Pharen"]
|
|
123
159
|
end
|
|
124
160
|
|
|
125
161
|
def self.details
|
|
126
|
-
"
|
|
127
|
-
"upload
|
|
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
|
|
130
|
-
"Missing dSYMs
|
|
131
|
-
"symbols ships crashes that
|
|
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. `
|
|
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
|
|
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:
|
|
157
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
69
|
+
["Pharen"]
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
def self.details
|
|
73
|
-
"Thin wrapper over `pharen upload
|
|
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
|
|
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. `
|
|
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
|
|
95
|
+
description: "Fail the lane on error (true, default) instead of warning and continuing",
|
|
97
96
|
optional: true,
|
|
98
|
-
default_value:
|
|
99
|
-
|
|
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
|
|
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
|
-
#
|
|
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], ["
|
|
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
|
|
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
|
|
66
|
+
["Pharen"]
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
def self.details
|
|
70
|
-
"Thin wrapper over `pharen upload
|
|
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
|
|
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. `
|
|
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
|
|
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:
|
|
95
|
-
|
|
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:
|
|
13
|
-
#
|
|
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. "
|
|
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
|
-
|
|
109
|
-
|
|
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
|
|
125
|
-
"
|
|
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.
|
|
138
|
-
#
|
|
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
|
|
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
|
|
165
|
-
# a
|
|
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
|
-
|
|
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
|
|
|
@@ -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.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
- Pharen
|
|
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,
|
|
56
|
-
— it registers the release, uploads every dSYM,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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.
|
|
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'
|