fastlane-plugin-pharen 0.1.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 +7 -0
- data/CHANGELOG.md +31 -0
- data/LICENSE +21 -0
- data/README.md +132 -0
- data/lib/fastlane/plugin/pharen/actions/pharen_release_action.rb +176 -0
- data/lib/fastlane/plugin/pharen/actions/pharen_upload_build_action.rb +118 -0
- data/lib/fastlane/plugin/pharen/actions/pharen_upload_symbols_action.rb +108 -0
- data/lib/fastlane/plugin/pharen/helper/pharen_helper.rb +195 -0
- data/lib/fastlane/plugin/pharen/version.rb +5 -0
- data/lib/fastlane/plugin/pharen.rb +18 -0
- metadata +98 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8cd4f080adf3fef449642139ef7ca42ee6f186325ca8948ee05dc7fdab939c3a
|
|
4
|
+
data.tar.gz: 4db24993e8d906973ab4c91e8df26d26bd01104c00c332c3d2419a85b62f4b54
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a05b26ce9c1de6c6c1fa251948a7d6af63bd6e89bf4bef1338d7247c6ca37290e7dbbbe5864ab2d83f51450ed719b23ebdd2d6d993e9e0a4140e7891af2137c2
|
|
7
|
+
data.tar.gz: 3b6f71ab17e05f1b1f50488360a7533e2dbfe5757d0b36f536c42daef9f662f092be6a628f2b9e94bee3d4de2b9a4ff6671d6cd9dff164ad095e7253ea0b8f9a
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `fastlane-plugin-pharen` are documented here.
|
|
4
|
+
|
|
5
|
+
## 0.1.0 — initial release
|
|
6
|
+
|
|
7
|
+
Three actions, a thin wrapper over the `pharen` CLI (auth, `.pharen.yml` resolution,
|
|
8
|
+
debug-id parsing and server-side dedupe all live there):
|
|
9
|
+
|
|
10
|
+
- **`pharen_release`** — the whole integration in one call after `build_app`: registers
|
|
11
|
+
the release, uploads every dSYM the archive produced, uploads the build, and returns
|
|
12
|
+
the OTA install link. Zero-arg by design — IPA and dSYMs come from the lane context,
|
|
13
|
+
version/build/bundle id are read from the IPA's own `Info.plist`, the commit sha from
|
|
14
|
+
git, org/app from the committed `.pharen.yml`, auth from `PHAREN_AUTH_TOKEN`.
|
|
15
|
+
- **`pharen_upload_build`** — upload the IPA and get the install link back, standalone.
|
|
16
|
+
- **`pharen_upload_symbols`** — upload dSYMs for crash symbolication, standalone. Accepts
|
|
17
|
+
`.dSYM` bundles, dSYM `.zip` files (as produced by `gym`/`download_dsyms`), or raw
|
|
18
|
+
DWARF binaries.
|
|
19
|
+
|
|
20
|
+
**Failure posture:** warn-don't-fail by default — a telemetry step should never be the
|
|
21
|
+
reason a release build fails. Pass `strict: true` (or set `PHAREN_STRICT=1`) in CI lanes
|
|
22
|
+
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.
|
|
25
|
+
|
|
26
|
+
**Auth:** `PHAREN_AUTH_TOKEN` is read from the environment only — the plugin never
|
|
27
|
+
reads, prints, or passes it on a command line.
|
|
28
|
+
|
|
29
|
+
**Dependencies:** none beyond `fastlane` itself at runtime. The plugin shells out to the
|
|
30
|
+
`pharen` CLI and reads its JSON-on-stdout contract; all wire logic (auth, config
|
|
31
|
+
resolution, debug-id parsing, dedupe) lives in the CLI.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lucubra LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# fastlane-plugin-pharen
|
|
2
|
+
|
|
3
|
+
Ship an iOS build to Pharen from your Fastfile. One action, `pharen_release`, runs after
|
|
4
|
+
`build_app`: it registers the release, uploads every dSYM for crash symbolication,
|
|
5
|
+
uploads the build, and returns an OTA install link — with nothing to configure in the
|
|
6
|
+
lane.
|
|
7
|
+
|
|
8
|
+
The plugin is a thin wrapper over the `pharen` CLI. Authentication, `.pharen.yml`
|
|
9
|
+
resolution, debug-id parsing, and server-side dedupe all live in the CLI; the plugin only
|
|
10
|
+
derives what the CLI needs from the lane context and the built artifacts. It adds no
|
|
11
|
+
runtime gem dependencies of its own.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
fastlane add_plugin pharen
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Once per repository, you also need:
|
|
20
|
+
|
|
21
|
+
- the `pharen` CLI on `PATH` (or point `pharen_binary:` / `PHAREN_BINARY` at it),
|
|
22
|
+
- 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)).
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
lane :beta do
|
|
29
|
+
build_app(scheme: "MyApp")
|
|
30
|
+
pharen_release
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`pharen_release` derives everything else: the IPA and dSYMs from the lane context; the
|
|
35
|
+
version, build number, and bundle id from the IPA's own `Info.plist`; the commit sha
|
|
36
|
+
from git. It registers the release, uploads every dSYM the archive produced, uploads the
|
|
37
|
+
build, prints the OTA install link, and returns it (also available in
|
|
38
|
+
`lane_context[SharedValues::PHAREN_INSTALL_URL]`).
|
|
39
|
+
|
|
40
|
+
## Actions
|
|
41
|
+
|
|
42
|
+
### `pharen_release`
|
|
43
|
+
|
|
44
|
+
The whole integration in one call after `build_app`.
|
|
45
|
+
|
|
46
|
+
| Option | Env | Default | Description |
|
|
47
|
+
|---|---|---|---|
|
|
48
|
+
| `ipa` | `PHAREN_IPA` | lane context (`IPA_OUTPUT_PATH`) | Path to the `.ipa`. |
|
|
49
|
+
| `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. |
|
|
52
|
+
|
|
53
|
+
Returns the install link on success, or `nil` when a step is skipped or fails outside
|
|
54
|
+
strict mode.
|
|
55
|
+
|
|
56
|
+
### `pharen_upload_build`
|
|
57
|
+
|
|
58
|
+
Upload the IPA and get the install link back, standalone.
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
pharen_upload_build # IPA from the lane context → install link
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
| Option | Env | Default | Description |
|
|
65
|
+
|---|---|---|---|
|
|
66
|
+
| `ipa` | `PHAREN_IPA` | lane context (`IPA_OUTPUT_PATH`) | Path to the `.ipa`. |
|
|
67
|
+
| `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. |
|
|
69
|
+
|
|
70
|
+
### `pharen_upload_symbols`
|
|
71
|
+
|
|
72
|
+
Upload dSYMs for crash symbolication, standalone.
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
pharen_upload_symbols # dSYMs from the lane context
|
|
76
|
+
pharen_upload_symbols(dsym_paths: ["path/to/App.dSYM"]) # or explicit
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
| Option | Env | Default | Description |
|
|
80
|
+
|---|---|---|---|
|
|
81
|
+
| `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
|
+
| `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. |
|
|
84
|
+
|
|
85
|
+
## Configuration
|
|
86
|
+
|
|
87
|
+
Two things live outside the lane, and only one of them is a secret:
|
|
88
|
+
|
|
89
|
+
- **`.pharen.yml`** — committed at the repo root, holding `org:` and `app:`. No secrets
|
|
90
|
+
belong in it. The CLI walks up from its working directory to find it, so it resolves
|
|
91
|
+
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.
|
|
96
|
+
|
|
97
|
+
## Failure posture
|
|
98
|
+
|
|
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.
|
|
102
|
+
|
|
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.
|
|
105
|
+
|
|
106
|
+
Two things hold regardless of mode:
|
|
107
|
+
|
|
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.
|
|
112
|
+
- **Re-runs are safe.** The server dedupes symbols by debug id, and the CLI exits 0 when
|
|
113
|
+
every file is already uploaded — so retrying a lane never double-uploads or errors on
|
|
114
|
+
work already done.
|
|
115
|
+
|
|
116
|
+
## Troubleshooting
|
|
117
|
+
|
|
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`).
|
|
121
|
+
- **"no IPA" or "no dSYMs in the lane context".** Run the action after `build_app`/`gym`
|
|
122
|
+
(and `download_dsyms`, if you fetch symbols from App Store Connect), or pass `ipa:` /
|
|
123
|
+
`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.
|
|
127
|
+
- **Not using fastlane?** The `pharen` CLI runs the same three steps directly. See the
|
|
128
|
+
documentation at <https://pharen.ai/docs>.
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# pharen_release — the whole Pharen integration in ONE bare call after build_app:
|
|
2
|
+
# register the release, upload every dSYM, upload the IPA, return the install link.
|
|
3
|
+
#
|
|
4
|
+
# lane :beta do
|
|
5
|
+
# build_app(scheme: "MyApp")
|
|
6
|
+
# pharen_release
|
|
7
|
+
# end
|
|
8
|
+
#
|
|
9
|
+
# Zero-arg: the IPA and dSYMs default from the lane context (gym/download_dsyms);
|
|
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
|
|
14
|
+
# is never a step you have to remember to run.
|
|
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.
|
|
20
|
+
|
|
21
|
+
require "fastlane/action"
|
|
22
|
+
require_relative "../helper/pharen_helper"
|
|
23
|
+
|
|
24
|
+
module Fastlane
|
|
25
|
+
module Actions
|
|
26
|
+
module SharedValues
|
|
27
|
+
PHAREN_INSTALL_URL = :PHAREN_INSTALL_URL unless const_defined?(:PHAREN_INSTALL_URL)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class PharenReleaseAction < Action
|
|
31
|
+
def self.run(params)
|
|
32
|
+
helper = Helper::PharenHelper
|
|
33
|
+
|
|
34
|
+
# -- gather: IPA + identity (the customer passes nothing) --------------------
|
|
35
|
+
ipa = params[:ipa] || Actions.lane_context[SharedValues::IPA_OUTPUT_PATH]
|
|
36
|
+
if ipa.nil? || ipa.to_s.empty?
|
|
37
|
+
helper.fail_or_warn(params[:strict], "pharen_release: no IPA — run after build_app/gym, or pass ipa:")
|
|
38
|
+
return nil
|
|
39
|
+
end
|
|
40
|
+
ipa = File.expand_path(ipa.to_s)
|
|
41
|
+
|
|
42
|
+
begin
|
|
43
|
+
info = helper.read_ipa_info(ipa)
|
|
44
|
+
rescue Helper::PharenHelper::Failure => e
|
|
45
|
+
helper.fail_or_warn(params[:strict], "pharen_release: #{e.message}")
|
|
46
|
+
return nil
|
|
47
|
+
end
|
|
48
|
+
|
|
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
|
+
dsym_inputs = params[:dsym_paths]
|
|
63
|
+
if dsym_inputs.nil? || Array(dsym_inputs).empty?
|
|
64
|
+
candidates = []
|
|
65
|
+
candidates << Actions.lane_context[SharedValues::DSYM_OUTPUT_PATH]
|
|
66
|
+
candidates.concat(Array(Actions.lane_context[SharedValues::DSYM_PATHS]))
|
|
67
|
+
dsym_inputs = candidates.compact.uniq
|
|
68
|
+
end
|
|
69
|
+
if Array(dsym_inputs).empty?
|
|
70
|
+
helper.fail_or_warn(
|
|
71
|
+
params[:strict],
|
|
72
|
+
"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
|
+
)
|
|
76
|
+
else
|
|
77
|
+
begin
|
|
78
|
+
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
|
+
rescue Helper::PharenHelper::Failure => e
|
|
87
|
+
helper.fail_or_warn(params[:strict], "pharen_release: #{e.message}")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# -- step 3: the build itself → install link ----------------------------------
|
|
92
|
+
build_args = [
|
|
93
|
+
"upload-build",
|
|
94
|
+
"--platform", "ios",
|
|
95
|
+
"--version", info[:version],
|
|
96
|
+
"--build", info[:build],
|
|
97
|
+
"--bundle-id", info[:bundle_id],
|
|
98
|
+
ipa
|
|
99
|
+
]
|
|
100
|
+
build = helper.run_pharen(params[:pharen_binary], build_args)
|
|
101
|
+
unless build[:ok]
|
|
102
|
+
helper.fail_or_warn(params[:strict], helper.cli_failure_message("pharen upload-build", build))
|
|
103
|
+
return nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
install_url = build[:json].is_a?(Hash) ? build[:json]["install_url"] : nil
|
|
107
|
+
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")
|
|
109
|
+
UI.important("Install (Safari on the phone): #{install_url}") if install_url
|
|
110
|
+
install_url
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
#####################################################
|
|
114
|
+
# @!group Documentation
|
|
115
|
+
#####################################################
|
|
116
|
+
|
|
117
|
+
def self.description
|
|
118
|
+
"One call after build_app: register the release, upload symbols + build to Pharen, return the install link"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.authors
|
|
122
|
+
["Pharen (Lucubra LLC)"]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
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, " \
|
|
128
|
+
"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."
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def self.available_options
|
|
135
|
+
[
|
|
136
|
+
FastlaneCore::ConfigItem.new(key: :ipa,
|
|
137
|
+
env_name: "PHAREN_IPA",
|
|
138
|
+
description: "Path to the .ipa. Defaults to what gym/build_app left in the lane context",
|
|
139
|
+
optional: true,
|
|
140
|
+
type: String),
|
|
141
|
+
FastlaneCore::ConfigItem.new(key: :dsym_paths,
|
|
142
|
+
env_name: "PHAREN_DSYM_PATHS",
|
|
143
|
+
description: "dSYM bundles/zips to upload. Defaults to gym's DSYM_OUTPUT_PATH + download_dsyms' DSYM_PATHS",
|
|
144
|
+
optional: true,
|
|
145
|
+
is_string: false),
|
|
146
|
+
FastlaneCore::ConfigItem.new(key: :pharen_binary,
|
|
147
|
+
env_name: "PHAREN_BINARY",
|
|
148
|
+
description: "Command that invokes the Pharen CLI (may include arguments, e.g. `node /path/to/cli.js`)",
|
|
149
|
+
optional: true,
|
|
150
|
+
default_value: "pharen",
|
|
151
|
+
type: String),
|
|
152
|
+
FastlaneCore::ConfigItem.new(key: :strict,
|
|
153
|
+
env_name: "PHAREN_STRICT",
|
|
154
|
+
description: "Fail the lane on any step's error (true) instead of warning and continuing (false)",
|
|
155
|
+
optional: true,
|
|
156
|
+
default_value: false,
|
|
157
|
+
is_string: false)
|
|
158
|
+
]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def self.output
|
|
162
|
+
[
|
|
163
|
+
["PHAREN_INSTALL_URL", "The itms-services install link answered by the Pharen control plane"]
|
|
164
|
+
]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def self.return_value
|
|
168
|
+
"The install_url string on success; nil on skipped/failed-but-not-strict"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def self.is_supported?(platform)
|
|
172
|
+
platform == :ios
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# pharen_upload_build — upload the IPA to Pharen's dl distribution and get the
|
|
2
|
+
# install link back.
|
|
3
|
+
#
|
|
4
|
+
# Zero-arg capable: the IPA defaults from gym/build_app's SharedValues::IPA_OUTPUT_PATH,
|
|
5
|
+
# and version / build number / bundle id are read out of the IPA's own Info.plist
|
|
6
|
+
# (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).
|
|
8
|
+
|
|
9
|
+
require "fastlane/action"
|
|
10
|
+
require_relative "../helper/pharen_helper"
|
|
11
|
+
|
|
12
|
+
module Fastlane
|
|
13
|
+
module Actions
|
|
14
|
+
module SharedValues
|
|
15
|
+
PHAREN_INSTALL_URL = :PHAREN_INSTALL_URL unless const_defined?(:PHAREN_INSTALL_URL)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class PharenUploadBuildAction < Action
|
|
19
|
+
def self.run(params)
|
|
20
|
+
helper = Helper::PharenHelper
|
|
21
|
+
|
|
22
|
+
ipa = params[:ipa] || Actions.lane_context[SharedValues::IPA_OUTPUT_PATH]
|
|
23
|
+
if ipa.nil? || ipa.to_s.empty?
|
|
24
|
+
helper.fail_or_warn(
|
|
25
|
+
params[:strict],
|
|
26
|
+
"pharen_upload_build: no IPA — run after build_app/gym, or pass ipa:"
|
|
27
|
+
)
|
|
28
|
+
return nil
|
|
29
|
+
end
|
|
30
|
+
ipa = File.expand_path(ipa.to_s)
|
|
31
|
+
|
|
32
|
+
begin
|
|
33
|
+
info = helper.read_ipa_info(ipa)
|
|
34
|
+
rescue Helper::PharenHelper::Failure => e
|
|
35
|
+
helper.fail_or_warn(params[:strict], "pharen_upload_build: #{e.message}")
|
|
36
|
+
return nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
args = [
|
|
40
|
+
"upload-build",
|
|
41
|
+
"--platform", "ios",
|
|
42
|
+
"--version", info[:version],
|
|
43
|
+
"--build", info[:build],
|
|
44
|
+
"--bundle-id", info[:bundle_id],
|
|
45
|
+
ipa
|
|
46
|
+
]
|
|
47
|
+
result = helper.run_pharen(params[:pharen_binary], args)
|
|
48
|
+
unless result[:ok]
|
|
49
|
+
helper.fail_or_warn(params[:strict], helper.cli_failure_message("pharen upload-build", result))
|
|
50
|
+
return nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
install_url = result[:json].is_a?(Hash) ? result[:json]["install_url"] : nil
|
|
54
|
+
Actions.lane_context[SharedValues::PHAREN_INSTALL_URL] = install_url if install_url
|
|
55
|
+
UI.success("pharen_upload_build: uploaded #{info[:version]} (#{info[:build]}) #{info[:bundle_id]}")
|
|
56
|
+
UI.important("Install (Safari on the phone): #{install_url}") if install_url
|
|
57
|
+
install_url
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
#####################################################
|
|
61
|
+
# @!group Documentation
|
|
62
|
+
#####################################################
|
|
63
|
+
|
|
64
|
+
def self.description
|
|
65
|
+
"Upload the IPA to Pharen dl distribution and return the OTA install link (zero-arg after gym)"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.authors
|
|
69
|
+
["Pharen (Lucubra LLC)"]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.details
|
|
73
|
+
"Thin wrapper over `pharen upload-build`: the IPA defaults from " \
|
|
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 " \
|
|
77
|
+
"install_url is this action's return value and lands in " \
|
|
78
|
+
"lane_context[SharedValues::PHAREN_INSTALL_URL]."
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.available_options
|
|
82
|
+
[
|
|
83
|
+
FastlaneCore::ConfigItem.new(key: :ipa,
|
|
84
|
+
env_name: "PHAREN_IPA",
|
|
85
|
+
description: "Path to the .ipa. Defaults to what gym/build_app left in the lane context",
|
|
86
|
+
optional: true,
|
|
87
|
+
type: String),
|
|
88
|
+
FastlaneCore::ConfigItem.new(key: :pharen_binary,
|
|
89
|
+
env_name: "PHAREN_BINARY",
|
|
90
|
+
description: "Command that invokes the Pharen CLI (may include arguments, e.g. `node /path/to/cli.js`)",
|
|
91
|
+
optional: true,
|
|
92
|
+
default_value: "pharen",
|
|
93
|
+
type: String),
|
|
94
|
+
FastlaneCore::ConfigItem.new(key: :strict,
|
|
95
|
+
env_name: "PHAREN_STRICT",
|
|
96
|
+
description: "Fail the lane on error (true) instead of warning and continuing (false)",
|
|
97
|
+
optional: true,
|
|
98
|
+
default_value: false,
|
|
99
|
+
is_string: false)
|
|
100
|
+
]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.output
|
|
104
|
+
[
|
|
105
|
+
["PHAREN_INSTALL_URL", "The itms-services install link answered by the Pharen control plane"]
|
|
106
|
+
]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def self.return_value
|
|
110
|
+
"The install_url string on success; nil on skipped/failed-but-not-strict"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self.is_supported?(platform)
|
|
114
|
+
platform == :ios
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# pharen_upload_symbols — upload dSYMs to Pharen for crash symbolication.
|
|
2
|
+
#
|
|
3
|
+
# Zero-arg capable: with no options it takes the dSYMs the lane already produced —
|
|
4
|
+
# gym/build_app's SharedValues::DSYM_OUTPUT_PATH and download_dsyms'
|
|
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).
|
|
8
|
+
|
|
9
|
+
require "fastlane/action"
|
|
10
|
+
require_relative "../helper/pharen_helper"
|
|
11
|
+
|
|
12
|
+
module Fastlane
|
|
13
|
+
module Actions
|
|
14
|
+
class PharenUploadSymbolsAction < Action
|
|
15
|
+
def self.run(params)
|
|
16
|
+
helper = Helper::PharenHelper
|
|
17
|
+
|
|
18
|
+
paths = params[:dsym_paths]
|
|
19
|
+
paths = default_dsym_paths if paths.nil? || Array(paths).empty?
|
|
20
|
+
if Array(paths).empty?
|
|
21
|
+
helper.fail_or_warn(
|
|
22
|
+
params[:strict],
|
|
23
|
+
"pharen_upload_symbols: no dSYMs to upload — run after build_app/gym or " \
|
|
24
|
+
"download_dsyms, or pass dsym_paths"
|
|
25
|
+
)
|
|
26
|
+
return nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
begin
|
|
30
|
+
expanded = helper.expand_dsym_inputs(paths)
|
|
31
|
+
rescue Helper::PharenHelper::Failure => e
|
|
32
|
+
helper.fail_or_warn(params[:strict], "pharen_upload_symbols: #{e.message}")
|
|
33
|
+
return nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
result = helper.run_pharen(params[:pharen_binary], ["upload-symbols"] + expanded)
|
|
37
|
+
unless result[:ok]
|
|
38
|
+
helper.fail_or_warn(params[:strict], helper.cli_failure_message("pharen upload-symbols", result))
|
|
39
|
+
return nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
summary = result[:json].is_a?(Hash) ? result[:json]["summary"] : nil
|
|
43
|
+
UI.success("pharen_upload_symbols: #{summary || 'symbols uploaded'}")
|
|
44
|
+
result[:json]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The two places fastlane already put dSYMs, in build order: gym's zip, then any
|
|
48
|
+
# App Store Connect downloads. Duplicates collapse; the CLI dedupes by debug id
|
|
49
|
+
# anyway (re-uploads answer 'duplicate' and exit 0, so this is always re-run safe).
|
|
50
|
+
def self.default_dsym_paths
|
|
51
|
+
candidates = []
|
|
52
|
+
candidates << Actions.lane_context[SharedValues::DSYM_OUTPUT_PATH]
|
|
53
|
+
candidates.concat(Array(Actions.lane_context[SharedValues::DSYM_PATHS]))
|
|
54
|
+
candidates.compact.map(&:to_s).reject(&:empty?).uniq
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
#####################################################
|
|
58
|
+
# @!group Documentation
|
|
59
|
+
#####################################################
|
|
60
|
+
|
|
61
|
+
def self.description
|
|
62
|
+
"Upload dSYMs to Pharen for crash symbolication (zero-arg after gym/download_dsyms)"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.authors
|
|
66
|
+
["Pharen (Lucubra LLC)"]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.details
|
|
70
|
+
"Thin wrapper over `pharen upload-symbols`: dSYM paths default from " \
|
|
71
|
+
"SharedValues::DSYM_OUTPUT_PATH (gym) and SharedValues::DSYM_PATHS " \
|
|
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. " \
|
|
74
|
+
"Server dedupes by debug id, so re-runs are safe."
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.available_options
|
|
78
|
+
[
|
|
79
|
+
FastlaneCore::ConfigItem.new(key: :dsym_paths,
|
|
80
|
+
env_name: "PHAREN_DSYM_PATHS",
|
|
81
|
+
description: "dSYM inputs (.dSYM bundles, dSYM .zip files, or raw DWARF binaries). Defaults to what gym/download_dsyms left in the lane context",
|
|
82
|
+
optional: true,
|
|
83
|
+
type: Array),
|
|
84
|
+
FastlaneCore::ConfigItem.new(key: :pharen_binary,
|
|
85
|
+
env_name: "PHAREN_BINARY",
|
|
86
|
+
description: "Command that invokes the Pharen CLI (may include arguments, e.g. `node /path/to/cli.js`)",
|
|
87
|
+
optional: true,
|
|
88
|
+
default_value: "pharen",
|
|
89
|
+
type: String),
|
|
90
|
+
FastlaneCore::ConfigItem.new(key: :strict,
|
|
91
|
+
env_name: "PHAREN_STRICT",
|
|
92
|
+
description: "Fail the lane on error (true) instead of warning and continuing (false)",
|
|
93
|
+
optional: true,
|
|
94
|
+
default_value: false,
|
|
95
|
+
is_string: false)
|
|
96
|
+
]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.return_value
|
|
100
|
+
"The CLI's parsed JSON ({ archives, summary }) on success; nil on skipped/failed-but-not-strict"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.is_supported?(platform)
|
|
104
|
+
platform == :ios
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# PharenHelper — everything the pharen_* actions share. The plugin is a THIN wrapper:
|
|
2
|
+
# all wire logic (auth, .pharen.yml, debug-id parsing, dedupe) lives in the `pharen`
|
|
3
|
+
# CLI. This helper only
|
|
4
|
+
# * derives metadata the CLI needs (Info.plist out of the IPA, git commit sha),
|
|
5
|
+
# * normalizes dSYM inputs (gym/download_dsyms hand over .zip files; the CLI wants
|
|
6
|
+
# .dSYM bundles or raw DWARF binaries),
|
|
7
|
+
# * shells out to the CLI and interprets its JSON-on-stdout contract.
|
|
8
|
+
#
|
|
9
|
+
# Dependency-free by design: macOS system tools only (unzip, plutil, git) — every box
|
|
10
|
+
# that can produce an IPA has them — so the gem adds NO runtime dependencies.
|
|
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.
|
|
14
|
+
|
|
15
|
+
require "fastlane_core/ui/ui"
|
|
16
|
+
require "json"
|
|
17
|
+
require "open3"
|
|
18
|
+
require "shellwords"
|
|
19
|
+
require "tmpdir"
|
|
20
|
+
|
|
21
|
+
module Fastlane
|
|
22
|
+
UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
|
|
23
|
+
|
|
24
|
+
module Helper
|
|
25
|
+
class PharenHelper
|
|
26
|
+
# Raised for local preparation problems (bad IPA, missing dSYM, unzip failure).
|
|
27
|
+
# Actions translate it via `fail_or_warn` — never let it escape raw.
|
|
28
|
+
class Failure < StandardError; end
|
|
29
|
+
|
|
30
|
+
# The app's own Info.plist sits exactly at Payload/<Name>.app/Info.plist.
|
|
31
|
+
# [^/]+ cannot span "/", so nested bundles (Watch apps, app extensions,
|
|
32
|
+
# frameworks) never match.
|
|
33
|
+
INFO_PLIST_ENTRY = %r{\APayload/[^/]+\.app/Info\.plist\z}
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
# -- IPA introspection ---------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
# IPA -> { version:, build:, bundle_id: } (CFBundleShortVersionString,
|
|
39
|
+
# CFBundleVersion, CFBundleIdentifier), so the customer passes nothing.
|
|
40
|
+
# unzip streams the (usually binary) plist; plutil converts it to JSON on stdout.
|
|
41
|
+
def read_ipa_info(ipa_path)
|
|
42
|
+
raise Failure, "IPA not found at #{ipa_path}" unless File.exist?(ipa_path)
|
|
43
|
+
|
|
44
|
+
entry = info_plist_entry(ipa_path)
|
|
45
|
+
raw, _err, status = capture(["unzip", "-p", ipa_path, entry])
|
|
46
|
+
raise Failure, "could not read #{entry} from #{ipa_path} (unzip exit #{status})" unless status == 0
|
|
47
|
+
|
|
48
|
+
json, perr, pstatus = capture(["plutil", "-convert", "json", "-o", "-", "-"], stdin: raw)
|
|
49
|
+
raise Failure, "plutil could not parse Info.plist from #{ipa_path}: #{perr.to_s.strip}" unless pstatus == 0
|
|
50
|
+
|
|
51
|
+
plist = JSON.parse(json.force_encoding("UTF-8"))
|
|
52
|
+
info = {
|
|
53
|
+
version: plist["CFBundleShortVersionString"],
|
|
54
|
+
build: plist["CFBundleVersion"],
|
|
55
|
+
bundle_id: plist["CFBundleIdentifier"]
|
|
56
|
+
}
|
|
57
|
+
missing = info.select { |_k, v| v.nil? || v.to_s.empty? }.keys
|
|
58
|
+
unless missing.empty?
|
|
59
|
+
raise Failure, "Info.plist in #{ipa_path} is missing #{missing.join(', ')}"
|
|
60
|
+
end
|
|
61
|
+
info
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Zip entry name of the app's Info.plist (shallowest Payload/*.app only).
|
|
65
|
+
def info_plist_entry(ipa_path)
|
|
66
|
+
listing, err, status = capture(["unzip", "-Z1", ipa_path])
|
|
67
|
+
raise Failure, "could not list #{ipa_path} (unzip -Z1 exit #{status}): #{err.to_s.strip}" unless status == 0
|
|
68
|
+
|
|
69
|
+
entry = listing.split("\n").find { |line| line.match?(INFO_PLIST_ENTRY) }
|
|
70
|
+
raise Failure, "no Payload/*.app/Info.plist inside #{ipa_path} — is this an IPA?" unless entry
|
|
71
|
+
entry
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# -- dSYM normalization --------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
# gym's DSYM_OUTPUT_PATH and download_dsyms' DSYM_PATHS are .zip files; the CLI
|
|
77
|
+
# takes .dSYM bundles (or raw DWARF binaries). Expand zips, absolutize the rest —
|
|
78
|
+
# paths must be absolute because run_pharen may chdir for .pharen.yml resolution.
|
|
79
|
+
def expand_dsym_inputs(paths)
|
|
80
|
+
Array(paths).flat_map do |path|
|
|
81
|
+
abs = File.expand_path(path.to_s)
|
|
82
|
+
raise Failure, "dSYM path not found: #{path}" unless File.exist?(abs)
|
|
83
|
+
abs.end_with?(".zip") ? unzip_dsyms(abs) : [abs]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Unpack a dSYM zip into a fresh temp dir and return the .dSYM bundles inside.
|
|
88
|
+
# The temp dir is left for the OS to reap — the CLI reads from it right after.
|
|
89
|
+
def unzip_dsyms(zip_path)
|
|
90
|
+
dest = Dir.mktmpdir("pharen-dsyms-")
|
|
91
|
+
_out, err, status = capture(["unzip", "-q", "-o", zip_path, "-d", dest])
|
|
92
|
+
raise Failure, "could not unzip #{zip_path}: #{err.to_s.strip}" unless status == 0
|
|
93
|
+
|
|
94
|
+
found = Dir.glob(File.join(dest, "**", "*.dSYM")).sort
|
|
95
|
+
raise Failure, "no .dSYM bundles inside #{zip_path}" if found.empty?
|
|
96
|
+
found
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# -- Pharen CLI invocation -----------------------------------------------------
|
|
100
|
+
|
|
101
|
+
# Run `<binary> <args...>` and interpret the CLI's contract: JSON on stdout,
|
|
102
|
+
# 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.
|
|
105
|
+
def run_pharen(binary, args, chdir: nil)
|
|
106
|
+
argv = Shellwords.split(binary.to_s) + args.map(&:to_s)
|
|
107
|
+
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)
|
|
110
|
+
json = parse_json(out)
|
|
111
|
+
{ ok: status == 0, status: status, stdout: out, stderr: err, json: json }
|
|
112
|
+
rescue Errno::ENOENT => e
|
|
113
|
+
# Spawn failed: binary (or chdir target) does not exist.
|
|
114
|
+
{ ok: false, status: nil, stdout: "", stderr: e.message, json: nil, not_found: true }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# One human line for a failed run_pharen result. Prefers the CLI's own
|
|
118
|
+
# { error: { code, message } } JSON; falls back to stderr, then the exit code.
|
|
119
|
+
def cli_failure_message(step, result)
|
|
120
|
+
error = result[:json].is_a?(Hash) ? result[:json]["error"] : nil
|
|
121
|
+
if error.is_a?(Hash) && error["message"]
|
|
122
|
+
"#{step} failed: #{error['message']} (#{error['code']})"
|
|
123
|
+
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"
|
|
126
|
+
else
|
|
127
|
+
detail = result[:stderr].to_s.strip
|
|
128
|
+
detail = result[:stdout].to_s.strip if detail.empty?
|
|
129
|
+
suffix = detail.empty? ? "" : ": #{detail.split("\n").last(3).join(' / ')}"
|
|
130
|
+
"#{step} failed (exit #{result[:status]})#{suffix}"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Where to run the CLI so it finds the customer's committed .pharen.yml.
|
|
135
|
+
# fastlane executes lanes from the fastlane/ directory (Dir.chdir in
|
|
136
|
+
# 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.)
|
|
142
|
+
def project_dir
|
|
143
|
+
cwd = Dir.pwd
|
|
144
|
+
return cwd if File.exist?(File.join(cwd, ".pharen.yml"))
|
|
145
|
+
parent = File.expand_path("..", cwd)
|
|
146
|
+
if File.basename(cwd) == "fastlane" && File.exist?(File.join(parent, ".pharen.yml"))
|
|
147
|
+
parent
|
|
148
|
+
else
|
|
149
|
+
cwd
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# -- misc ------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
# HEAD sha for `releases new --commit`; nil outside a git checkout (fine — the
|
|
156
|
+
# flag is optional provenance).
|
|
157
|
+
def git_commit_sha
|
|
158
|
+
out, _err, status = capture(%w[git rev-parse HEAD])
|
|
159
|
+
return nil unless status == 0
|
|
160
|
+
sha = out.strip
|
|
161
|
+
sha.empty? ? nil : sha
|
|
162
|
+
end
|
|
163
|
+
|
|
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.
|
|
166
|
+
def fail_or_warn(strict, message)
|
|
167
|
+
if strict
|
|
168
|
+
UI.user_error!(message)
|
|
169
|
+
else
|
|
170
|
+
UI.important("#{message} — continuing because strict is false")
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# -- plumbing (the single shell-out seam; specs stub exactly this) ---------------
|
|
175
|
+
|
|
176
|
+
# argv -> [stdout, stderr, exitstatus]; binmode because plists are binary.
|
|
177
|
+
def capture(argv, stdin: nil, chdir: nil)
|
|
178
|
+
options = { binmode: true }
|
|
179
|
+
options[:stdin_data] = stdin unless stdin.nil?
|
|
180
|
+
options[:chdir] = chdir unless chdir.nil?
|
|
181
|
+
out, err, status = Open3.capture3(*argv, **options)
|
|
182
|
+
[out, err, status.exitstatus]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
def parse_json(text)
|
|
188
|
+
JSON.parse(text)
|
|
189
|
+
rescue JSON::ParserError, TypeError
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# fastlane-plugin-pharen — standard fastlane plugin entry point (`fastlane add_plugin pharen`).
|
|
2
|
+
# Loads every action + helper under this directory; fastlane discovers actions by class name.
|
|
3
|
+
|
|
4
|
+
require "fastlane/plugin/pharen/version"
|
|
5
|
+
|
|
6
|
+
module Fastlane
|
|
7
|
+
module Pharen
|
|
8
|
+
# Return all .rb files inside the "actions" and "helper" directory.
|
|
9
|
+
def self.all_classes
|
|
10
|
+
Dir[File.expand_path("**/{actions,helper}/*.rb", File.dirname(__FILE__))]
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# By default we want to import all available actions and helpers.
|
|
16
|
+
Fastlane::Pharen.all_classes.each do |current|
|
|
17
|
+
require current
|
|
18
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fastlane-plugin-pharen
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Pharen (Lucubra LLC)
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: fastlane
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 2.220.0
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 2.220.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
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.'
|
|
60
|
+
email: hello@pharen.ai
|
|
61
|
+
executables: []
|
|
62
|
+
extensions: []
|
|
63
|
+
extra_rdoc_files: []
|
|
64
|
+
files:
|
|
65
|
+
- CHANGELOG.md
|
|
66
|
+
- LICENSE
|
|
67
|
+
- README.md
|
|
68
|
+
- lib/fastlane/plugin/pharen.rb
|
|
69
|
+
- lib/fastlane/plugin/pharen/actions/pharen_release_action.rb
|
|
70
|
+
- lib/fastlane/plugin/pharen/actions/pharen_upload_build_action.rb
|
|
71
|
+
- lib/fastlane/plugin/pharen/actions/pharen_upload_symbols_action.rb
|
|
72
|
+
- lib/fastlane/plugin/pharen/helper/pharen_helper.rb
|
|
73
|
+
- lib/fastlane/plugin/pharen/version.rb
|
|
74
|
+
homepage: https://pharen.ai
|
|
75
|
+
licenses:
|
|
76
|
+
- MIT
|
|
77
|
+
metadata:
|
|
78
|
+
homepage_uri: https://pharen.ai
|
|
79
|
+
rubygems_mfa_required: 'true'
|
|
80
|
+
rdoc_options: []
|
|
81
|
+
require_paths:
|
|
82
|
+
- lib
|
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '3.0'
|
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
89
|
+
requirements:
|
|
90
|
+
- - ">="
|
|
91
|
+
- !ruby/object:Gem::Version
|
|
92
|
+
version: '0'
|
|
93
|
+
requirements: []
|
|
94
|
+
rubygems_version: 4.0.11
|
|
95
|
+
specification_version: 4
|
|
96
|
+
summary: 'Pharen build distribution + crash symbols from your lane: pharen_release
|
|
97
|
+
/ pharen_upload_build / pharen_upload_symbols'
|
|
98
|
+
test_files: []
|