Class: Fastlane::Helper::PharenHelper

Inherits:
Object
  • Object
show all
Defined in:
lib/fastlane/plugin/pharen/helper/pharen_helper.rb

Defined Under Namespace

Classes: Failure

Constant Summary collapse

INFO_PLIST_ENTRY =

The app's own Info.plist sits exactly at Payload/.app/Info.plist. [^/]+ cannot span "/", so nested bundles (Watch apps, app extensions, frameworks) never match.

%r{\APayload/[^/]+\.app/Info\.plist\z}

Class Method Summary collapse

Class Method Details

.capture(argv, stdin: nil, chdir: nil) ⇒ Object

argv -> [stdout, stderr, exitstatus]; binmode because plists are binary.



177
178
179
180
181
182
183
# File 'lib/fastlane/plugin/pharen/helper/pharen_helper.rb', line 177

def capture(argv, stdin: nil, chdir: nil)
  options = { binmode: true }
  options[:stdin_data] = stdin unless stdin.nil?
  options[:chdir] = chdir unless chdir.nil?
  out, err, status = Open3.capture3(*argv, **options)
  [out, err, status.exitstatus]
end

.cli_failure_message(step, result) ⇒ Object

One human line for a failed run_pharen result. Prefers the CLI's own { error: { code, message } } JSON; falls back to stderr, then the exit code.



119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/fastlane/plugin/pharen/helper/pharen_helper.rb', line 119

def cli_failure_message(step, result)
  error = result[:json].is_a?(Hash) ? result[:json]["error"] : nil
  if error.is_a?(Hash) && error["message"]
    "#{step} failed: #{error['message']} (#{error['code']})"
  elsif result[:not_found]
    "#{step} failed: #{result[:stderr]} — install the Pharen CLI and ensure `pharen` is on PATH " \
      "(see https://pharen.ai/docs for install instructions), or set the pharen_binary option"
  else
    detail = result[:stderr].to_s.strip
    detail = result[:stdout].to_s.strip if detail.empty?
    suffix = detail.empty? ? "" : ": #{detail.split("\n").last(3).join(' / ')}"
    "#{step} failed (exit #{result[:status]})#{suffix}"
  end
end

.expand_dsym_inputs(paths) ⇒ Object

gym's DSYM_OUTPUT_PATH and download_dsyms' DSYM_PATHS are .zip files; the CLI takes .dSYM bundles (or raw DWARF binaries). Expand zips, absolutize the rest — paths must be absolute because run_pharen may chdir for .pharen.yml resolution.



79
80
81
82
83
84
85
# File 'lib/fastlane/plugin/pharen/helper/pharen_helper.rb', line 79

def expand_dsym_inputs(paths)
  Array(paths).flat_map do |path|
    abs = File.expand_path(path.to_s)
    raise Failure, "dSYM path not found: #{path}" unless File.exist?(abs)
    abs.end_with?(".zip") ? unzip_dsyms(abs) : [abs]
  end
end

.fail_or_warn(strict, message) ⇒ Object

strict: abort the lane; default: warn loudly and let the build continue — a telemetry step should never be the reason a release build fails.



166
167
168
169
170
171
172
# File 'lib/fastlane/plugin/pharen/helper/pharen_helper.rb', line 166

def fail_or_warn(strict, message)
  if strict
    UI.user_error!(message)
  else
    UI.important("#{message} — continuing because strict is false")
  end
end

.git_commit_shaObject

HEAD sha for releases new --commit; nil outside a git checkout (fine — the flag is optional provenance).



157
158
159
160
161
162
# File 'lib/fastlane/plugin/pharen/helper/pharen_helper.rb', line 157

def git_commit_sha
  out, _err, status = capture(%w[git rev-parse HEAD])
  return nil unless status == 0
  sha = out.strip
  sha.empty? ? nil : sha
end

.info_plist_entry(ipa_path) ⇒ Object

Zip entry name of the app's Info.plist (shallowest Payload/*.app only).

Raises:



65
66
67
68
69
70
71
72
# File 'lib/fastlane/plugin/pharen/helper/pharen_helper.rb', line 65

def info_plist_entry(ipa_path)
  listing, err, status = capture(["unzip", "-Z1", ipa_path])
  raise Failure, "could not list #{ipa_path} (unzip -Z1 exit #{status}): #{err.to_s.strip}" unless status == 0

  entry = listing.split("\n").find { |line| line.match?(INFO_PLIST_ENTRY) }
  raise Failure, "no Payload/*.app/Info.plist inside #{ipa_path} — is this an IPA?" unless entry
  entry
end

.project_dirObject

Where to run the CLI so it finds the customer's committed .pharen.yml. fastlane executes lanes from the fastlane/ directory (Dir.chdir in LaneManager), but .pharen.yml lives at the repo root by convention — step up one level when that is clearly the layout. Explicit --org/--app flags are the escape hatch for exotic layouts. (The CLI itself also walks up from its cwd looking for .pharen.yml, so this special-case is now a belt-and-suspenders optimization, not the only path — kept as-is since it still resolves correctly and needs no change.)



142
143
144
145
146
147
148
149
150
151
# File 'lib/fastlane/plugin/pharen/helper/pharen_helper.rb', line 142

def project_dir
  cwd = Dir.pwd
  return cwd if File.exist?(File.join(cwd, ".pharen.yml"))
  parent = File.expand_path("..", cwd)
  if File.basename(cwd) == "fastlane" && File.exist?(File.join(parent, ".pharen.yml"))
    parent
  else
    cwd
  end
end

.read_ipa_info(ipa_path) ⇒ Object

IPA -> { version:, build:, bundle_id: } (CFBundleShortVersionString, CFBundleVersion, CFBundleIdentifier), so the customer passes nothing. unzip streams the (usually binary) plist; plutil converts it to JSON on stdout.

Raises:



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/fastlane/plugin/pharen/helper/pharen_helper.rb', line 41

def read_ipa_info(ipa_path)
  raise Failure, "IPA not found at #{ipa_path}" unless File.exist?(ipa_path)

  entry = info_plist_entry(ipa_path)
  raw, _err, status = capture(["unzip", "-p", ipa_path, entry])
  raise Failure, "could not read #{entry} from #{ipa_path} (unzip exit #{status})" unless status == 0

  json, perr, pstatus = capture(["plutil", "-convert", "json", "-o", "-", "-"], stdin: raw)
  raise Failure, "plutil could not parse Info.plist from #{ipa_path}: #{perr.to_s.strip}" unless pstatus == 0

  plist = JSON.parse(json.force_encoding("UTF-8"))
  info = {
    version: plist["CFBundleShortVersionString"],
    build: plist["CFBundleVersion"],
    bundle_id: plist["CFBundleIdentifier"]
  }
  missing = info.select { |_k, v| v.nil? || v.to_s.empty? }.keys
  unless missing.empty?
    raise Failure, "Info.plist in #{ipa_path} is missing #{missing.join(', ')}"
  end
  info
end

.run_pharen(binary, args, chdir: nil) ⇒ Object

Run <binary> <args...> and interpret the CLI's contract: JSON on stdout, exit 0 on success. binary is Shellwords-split so pharen_binary may carry a command with arguments (e.g. "node /path/to/cli.js"). Never raises on CLI failure — callers decide strict-vs-warn from the returned hash.



105
106
107
108
109
110
111
112
113
114
115
# File 'lib/fastlane/plugin/pharen/helper/pharen_helper.rb', line 105

def run_pharen(binary, args, chdir: nil)
  argv = Shellwords.split(binary.to_s) + args.map(&:to_s)
  dir = chdir || project_dir
  UI.command(Shellwords.join(argv)) # never contains PHAREN_AUTH_TOKEN (env-only)
  out, err, status = capture(argv, chdir: dir)
  json = parse_json(out)
  { ok: status == 0, status: status, stdout: out, stderr: err, json: json }
rescue Errno::ENOENT => e
  # Spawn failed: binary (or chdir target) does not exist.
  { ok: false, status: nil, stdout: "", stderr: e.message, json: nil, not_found: true }
end

.unzip_dsyms(zip_path) ⇒ Object

Unpack a dSYM zip into a fresh temp dir and return the .dSYM bundles inside. The temp dir is left for the OS to reap — the CLI reads from it right after.

Raises:



89
90
91
92
93
94
95
96
97
# File 'lib/fastlane/plugin/pharen/helper/pharen_helper.rb', line 89

def unzip_dsyms(zip_path)
  dest = Dir.mktmpdir("pharen-dsyms-")
  _out, err, status = capture(["unzip", "-q", "-o", zip_path, "-d", dest])
  raise Failure, "could not unzip #{zip_path}: #{err.to_s.strip}" unless status == 0

  found = Dir.glob(File.join(dest, "**", "*.dSYM")).sort
  raise Failure, "no .dSYM bundles inside #{zip_path}" if found.empty?
  found
end