ruby_everywhere 0.6.0 → 0.7.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/exe/every +12 -1
- data/exe/rbe +12 -1
- data/lib/everywhere/agents_guide.rb +3 -1
- data/lib/everywhere/blake2b.rb +17 -1
- data/lib/everywhere/boot.rb +21 -4
- data/lib/everywhere/builders/android.rb +144 -30
- data/lib/everywhere/builders/desktop.rb +25 -17
- data/lib/everywhere/builders/ios.rb +244 -9
- data/lib/everywhere/cli.rb +2 -0
- data/lib/everywhere/commands/build.rb +113 -58
- data/lib/everywhere/commands/clean.rb +8 -3
- data/lib/everywhere/commands/dev.rb +85 -11
- data/lib/everywhere/commands/doctor.rb +138 -21
- data/lib/everywhere/commands/install.rb +48 -8
- data/lib/everywhere/commands/platform/build.rb +136 -27
- data/lib/everywhere/commands/platform/login.rb +16 -2
- data/lib/everywhere/commands/platform/runner.rb +113 -23
- data/lib/everywhere/commands/preview.rb +359 -0
- data/lib/everywhere/commands/publish.rb +20 -2
- data/lib/everywhere/commands/release.rb +145 -28
- data/lib/everywhere/commands/shell_dir.rb +2 -2
- data/lib/everywhere/config.rb +88 -3
- data/lib/everywhere/console.rb +2 -1
- data/lib/everywhere/engine.rb +24 -0
- data/lib/everywhere/framework.rb +21 -4
- data/lib/everywhere/ignore.rb +3 -1
- data/lib/everywhere/jump.rb +121 -0
- data/lib/everywhere/mobile_config_endpoint.rb +47 -0
- data/lib/everywhere/mobile_configs_controller.rb +46 -0
- data/lib/everywhere/paths.rb +13 -6
- data/lib/everywhere/platform/client.rb +27 -6
- data/lib/everywhere/platform/credentials.rb +18 -8
- data/lib/everywhere/platform/snapshot.rb +10 -0
- data/lib/everywhere/receipt.rb +55 -10
- data/lib/everywhere/shellout.rb +19 -0
- data/lib/everywhere/simulator.rb +12 -1
- data/lib/everywhere/version.rb +1 -1
- data/support/mobile/android/app/build.gradle.kts +29 -1
- data/support/mobile/ios/App/EverywhereConfig.swift +17 -5
- data/support/mobile/ios/App/SceneDelegate.swift +75 -8
- data/support/mobile/ios/App.xcodeproj/project.pbxproj +2 -2
- data/support/mobile/ios/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +0 -1
- data/support/mobile/ios/README.md +13 -6
- metadata +17 -1
|
@@ -21,8 +21,8 @@ module Everywhere
|
|
|
21
21
|
|
|
22
22
|
option :root, default: ".", desc: "App root directory"
|
|
23
23
|
option :url, default: nil, desc: "Platform base URL"
|
|
24
|
-
option :target, default: nil, desc: "Targets (comma-separated os-arch); overrides everywhere.yml"
|
|
25
|
-
option :channel, default: "direct", desc: "
|
|
24
|
+
option :target, default: nil, desc: "Targets (comma-separated os-arch[:channel]); overrides everywhere.yml"
|
|
25
|
+
option :channel, default: "direct", desc: "Default distribution channel for targets without a :channel suffix"
|
|
26
26
|
option :ruby, default: nil, desc: "Ruby version (default: build.ruby or 4.0.6)"
|
|
27
27
|
option :output, default: nil, desc: "Where to save artifacts (default: <root>/dist)"
|
|
28
28
|
option :wait, type: :boolean, default: true, desc: "Wait for the build and download results"
|
|
@@ -38,29 +38,26 @@ module Everywhere
|
|
|
38
38
|
|
|
39
39
|
client = Everywhere::Platform::Client.new(base, token: creds["token"])
|
|
40
40
|
framework = config.remote? ? nil : Everywhere::Framework.detect(root_path)
|
|
41
|
-
|
|
41
|
+
specs = resolve_targets(target, config, channel)
|
|
42
42
|
ruby ||= config.build_ruby || "4.0.6"
|
|
43
43
|
|
|
44
44
|
Dir.mktmpdir("every-snapshot") do |tmp|
|
|
45
45
|
signed_id = snapshot_and_upload(client, root_path, tmp)
|
|
46
|
-
|
|
47
|
-
root: root_path, config: config, framework: framework, ruby: ruby,
|
|
48
|
-
target: targets.first, channel: channel
|
|
49
|
-
).manifest
|
|
46
|
+
manifests = build_manifests(root_path, config, framework, ruby, specs)
|
|
50
47
|
|
|
51
|
-
build = submit(client,
|
|
52
|
-
UI.success("submitted #{UI.bold(build["id"])} for #{
|
|
48
|
+
build = submit(client, specs, channel, signed_id, manifests)
|
|
49
|
+
UI.success("submitted #{UI.bold(build["id"])} for #{specs.map(&:display_label).join(", ")}")
|
|
53
50
|
if (build_url = build["url"] || "#{client.base_url}/builds/#{build["id"]}")
|
|
54
51
|
UI.substep("watch it live → #{UI.cyan(build_url)}")
|
|
55
52
|
end
|
|
56
53
|
|
|
57
54
|
return unless wait
|
|
58
55
|
|
|
59
|
-
# Set expectations before the quiet part: a hosted
|
|
56
|
+
# Set expectations before the quiet part: a hosted runner is not
|
|
60
57
|
# instant, and knowing that up front is the difference between "slow"
|
|
61
58
|
# and "broken".
|
|
62
|
-
UI.substep(UI.dim("
|
|
63
|
-
final = poll_and_tail(client, build["id"])
|
|
59
|
+
UI.substep(UI.dim("#{runner_wait_subject(specs)} usually start within a few minutes"))
|
|
60
|
+
final = poll_and_tail(client, build["id"], build_url)
|
|
64
61
|
report(client, final, output || File.join(root_path, "dist"), download)
|
|
65
62
|
end
|
|
66
63
|
rescue Everywhere::Error => e
|
|
@@ -69,6 +66,30 @@ module Everywhere
|
|
|
69
66
|
|
|
70
67
|
private
|
|
71
68
|
|
|
69
|
+
# Which hosted runner OS each target builds on — not the target's own OS
|
|
70
|
+
# (iOS builds on macOS, Android on Linux).
|
|
71
|
+
RUNNER_OS_LABELS = {
|
|
72
|
+
"macos" => "macOS", "ios" => "macOS",
|
|
73
|
+
"android" => "Linux", "linux" => "Linux", "windows" => "Windows"
|
|
74
|
+
}.freeze
|
|
75
|
+
|
|
76
|
+
# One requested target. `token` is the literal string the user wrote
|
|
77
|
+
# ("ios-arm64:testflight") and is what keys the per-target manifests on the
|
|
78
|
+
# wire; `os_arch` is the bare form everything else (receipts, labels,
|
|
79
|
+
# filenames) is built from.
|
|
80
|
+
TargetSpec = Struct.new(:token, :os, :arch, :channel) do
|
|
81
|
+
def os_arch = "#{os}-#{arch}"
|
|
82
|
+
def label = UI.target_label(os_arch)
|
|
83
|
+
def display_label = channel == "direct" ? label : "#{label} (#{channel})"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def runner_wait_subject(specs)
|
|
87
|
+
kinds = specs.filter_map { |s| RUNNER_OS_LABELS[s.os] }.uniq
|
|
88
|
+
return "hosted build runners" if kinds.empty?
|
|
89
|
+
|
|
90
|
+
"hosted #{kinds.join("/")} runners"
|
|
91
|
+
end
|
|
92
|
+
|
|
72
93
|
def snapshot_and_upload(client, root_path, tmp)
|
|
73
94
|
path = File.join(tmp, "snapshot.tar.gz")
|
|
74
95
|
UI.step("snapshotting source #{UI.dim("(honoring .everywhereignore)")}")
|
|
@@ -79,6 +100,7 @@ module Everywhere
|
|
|
79
100
|
reservation = client.post("/cli/direct_uploads",
|
|
80
101
|
filename: "snapshot.tar.gz", byte_size: size,
|
|
81
102
|
checksum: Everywhere::Platform::Snapshot.md5_base64(path), content_type: "application/gzip")
|
|
103
|
+
die_unauthorized! if reservation.unauthorized?
|
|
82
104
|
UI.die!("couldn't reserve upload (HTTP #{reservation.status})") unless reservation.ok?
|
|
83
105
|
|
|
84
106
|
upload = reservation.body["direct_upload"] || {}
|
|
@@ -89,21 +111,49 @@ module Everywhere
|
|
|
89
111
|
reservation.body["signed_id"]
|
|
90
112
|
end
|
|
91
113
|
|
|
92
|
-
|
|
114
|
+
# `manifests` is keyed by the literal target token; `manifest` (singular)
|
|
115
|
+
# stays = the first target's, which is all an older Platform reads.
|
|
116
|
+
def submit(client, specs, channel, signed_id, manifests)
|
|
93
117
|
res = client.post("/cli/builds",
|
|
94
|
-
targets:
|
|
118
|
+
targets: specs.map(&:token).join(","), channel: channel, snapshot_signed_id: signed_id,
|
|
119
|
+
manifest: manifests.values.first, manifests: manifests)
|
|
95
120
|
return res.body if res.ok?
|
|
96
121
|
|
|
97
|
-
if res.
|
|
122
|
+
die_unauthorized! if res.unauthorized?
|
|
123
|
+
render_submit_error!(res)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def render_submit_error!(res)
|
|
127
|
+
body = res.body.is_a?(Hash) ? res.body : {}
|
|
128
|
+
|
|
129
|
+
case body["error"]
|
|
130
|
+
when "missing_credentials"
|
|
98
131
|
UI.bad("can't build yet — missing signing credentials:")
|
|
99
|
-
Array(
|
|
100
|
-
|
|
101
|
-
|
|
132
|
+
Array(body["targets"]).each do |t|
|
|
133
|
+
UI.step("#{server_target_label(t["target"])}: add #{Array(t["missing"]).join(", ")}")
|
|
134
|
+
end
|
|
135
|
+
UI.step("set them up → #{UI.cyan(body["manage_url"])}")
|
|
136
|
+
exit 1
|
|
137
|
+
when "invalid_targets"
|
|
138
|
+
UI.bad(body["message"] || "some targets can't be built as configured:")
|
|
139
|
+
Array(body["targets"]).each do |t|
|
|
140
|
+
UI.step("#{server_target_label(t["target"])}: #{Array(t["errors"]).join("; ")}")
|
|
102
141
|
end
|
|
103
|
-
UI.step("set them up → #{UI.cyan(res.body["manage_url"])}")
|
|
104
142
|
exit 1
|
|
143
|
+
when "unsupported_targets"
|
|
144
|
+
UI.step("supported: #{Array(body["supported"]).join(", ")}") if body["supported"]
|
|
105
145
|
end
|
|
106
|
-
|
|
146
|
+
|
|
147
|
+
UI.die!("build submission failed (HTTP #{res.status}) #{body["message"] || body["error"]}")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def server_target_label(target)
|
|
151
|
+
target ||= {}
|
|
152
|
+
"#{UI.os_label(target["os"])}-#{target["arch"]}/#{target["distribution_channel"]}"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def die_unauthorized!
|
|
156
|
+
UI.die!("token expired or revoked — run `every platform login`")
|
|
107
157
|
end
|
|
108
158
|
|
|
109
159
|
# How often we ask the Platform for new logs. A build spends most of its
|
|
@@ -114,14 +164,30 @@ module Everywhere
|
|
|
114
164
|
ACTIVE_POLL = 2
|
|
115
165
|
SPINNER_TICK = 0.2
|
|
116
166
|
|
|
117
|
-
|
|
167
|
+
# The build keeps running on the Platform whether or not we can reach it,
|
|
168
|
+
# so a blip mid-tail must not end the wait. Give up only after this many
|
|
169
|
+
# polls in a row have failed.
|
|
170
|
+
MAX_POLL_ERRORS = 5
|
|
171
|
+
|
|
172
|
+
def poll_and_tail(client, build_id, build_url = nil)
|
|
118
173
|
printed = Hash.new("")
|
|
119
174
|
last = {}
|
|
120
175
|
started = now
|
|
121
176
|
status = UI::Status.new
|
|
177
|
+
errors = 0
|
|
122
178
|
|
|
123
179
|
loop do
|
|
124
|
-
|
|
180
|
+
begin
|
|
181
|
+
res = client.get("/cli/builds/#{build_id}/logs")
|
|
182
|
+
errors = 0
|
|
183
|
+
rescue Everywhere::Error => e
|
|
184
|
+
errors += 1
|
|
185
|
+
status.clear
|
|
186
|
+
detach!(build_id, build_url, e) if errors >= MAX_POLL_ERRORS
|
|
187
|
+
UI.warn("lost contact with the Platform (#{e.message}) — retrying #{errors}/#{MAX_POLL_ERRORS}")
|
|
188
|
+
spin(status, [], started, IDLE_POLL)
|
|
189
|
+
next
|
|
190
|
+
end
|
|
125
191
|
artifacts = res.ok? ? (res.body["artifacts"] || []) : []
|
|
126
192
|
|
|
127
193
|
status.clear
|
|
@@ -140,6 +206,16 @@ module Everywhere
|
|
|
140
206
|
status&.clear
|
|
141
207
|
end
|
|
142
208
|
|
|
209
|
+
# We stopped watching; the build didn't stop. Leave the user everything
|
|
210
|
+
# they need to re-attach or grab the artifacts later.
|
|
211
|
+
def detach!(build_id, build_url, error)
|
|
212
|
+
UI.bad("gave up tailing #{UI.bold(build_id)} after #{MAX_POLL_ERRORS} failed polls — #{error.message}")
|
|
213
|
+
UI.step("the build is still running on the Platform")
|
|
214
|
+
UI.step("watch it → #{UI.cyan(build_url)}") if build_url
|
|
215
|
+
UI.die!("re-attach with `every platform build --wait` once you're back online, " \
|
|
216
|
+
"or download the artifacts from the build page")
|
|
217
|
+
end
|
|
218
|
+
|
|
143
219
|
# Hold the status line alive for `interval` seconds. The spinner ticks far
|
|
144
220
|
# faster than we poll so the line reads as "alive" during the multi-minute
|
|
145
221
|
# pre-runner wait rather than freezing between requests.
|
|
@@ -224,11 +300,44 @@ module Everywhere
|
|
|
224
300
|
end
|
|
225
301
|
end
|
|
226
302
|
|
|
227
|
-
# everywhere.yml build.targets, or --target, or the macOS default.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
303
|
+
# everywhere.yml build.targets, or --target, or the macOS default. Each
|
|
304
|
+
# entry is "os-arch" with an optional ":channel" suffix; --channel is the
|
|
305
|
+
# default for the ones that leave it off. No channel allowlist here — the
|
|
306
|
+
# Platform owns which channels exist and says so in its 422.
|
|
307
|
+
def resolve_targets(override, config, default_channel)
|
|
308
|
+
list = override ? override.split(",") : config.build_target_specs
|
|
309
|
+
list = Array(list).map { |t| t.to_s.strip }.reject(&:empty?)
|
|
310
|
+
list = ["macos-arm64"] if list.empty?
|
|
311
|
+
list.map { |token| parse_target_spec(token, default_channel) }
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def parse_target_spec(token, default_channel)
|
|
315
|
+
os_arch, channel, extra = token.split(":", -1)
|
|
316
|
+
if !extra.nil? || os_arch.to_s.empty? || (!channel.nil? && channel.empty?)
|
|
317
|
+
UI.die!("invalid target #{token.inspect} — expected os-arch[:channel], e.g. ios-arm64:testflight")
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
os, arch = os_arch.split("-", 2)
|
|
321
|
+
TargetSpec.new(token, os, arch, channel || default_channel)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# One manifest per target — they differ in `target`, and may differ in
|
|
325
|
+
# bundle_id/name/version via `platforms:` overrides. The source digest is
|
|
326
|
+
# the same for all of them, so hash the tree once and thread it through.
|
|
327
|
+
def build_manifests(root_path, config, framework, ruby, specs)
|
|
328
|
+
source = nil
|
|
329
|
+
lockfile = nil
|
|
330
|
+
|
|
331
|
+
specs.each_with_object({}) do |spec, out|
|
|
332
|
+
manifest = Everywhere::Receipt.new(
|
|
333
|
+
root: root_path, config: config, framework: framework, ruby: ruby,
|
|
334
|
+
target: spec.os_arch, channel: spec.channel,
|
|
335
|
+
source_checksum: source, lockfile_checksum: lockfile
|
|
336
|
+
).manifest
|
|
337
|
+
source ||= manifest.dig("source", "checksum")
|
|
338
|
+
lockfile ||= manifest.dig("source", "lockfile_checksum")
|
|
339
|
+
out[spec.token] = manifest
|
|
340
|
+
end
|
|
232
341
|
end
|
|
233
342
|
|
|
234
343
|
def label(target)
|
|
@@ -23,6 +23,14 @@ module Everywhere
|
|
|
23
23
|
start = client.post("/cli/device_authorizations", client_name: client_name)
|
|
24
24
|
UI.die!("couldn't start login — HTTP #{start.status}") unless start.ok?
|
|
25
25
|
|
|
26
|
+
# A captive portal or a misconfigured proxy answers 200 with HTML, which
|
|
27
|
+
# parses to {} — without this we'd print an empty code and poll nothing
|
|
28
|
+
# for ten minutes.
|
|
29
|
+
unless start.body["user_code"] && start.body["device_code"]
|
|
30
|
+
UI.die!("#{base} answered without a device code — check EVERYWHERE_PLATFORM_URL " \
|
|
31
|
+
"(a captive portal or proxy may be intercepting the request)")
|
|
32
|
+
end
|
|
33
|
+
|
|
26
34
|
verify_url = start.body["verification_uri_complete"] || start.body["verification_uri"]
|
|
27
35
|
UI.step("confirmation code: #{UI.bold(start.body["user_code"])}")
|
|
28
36
|
UI.step("opening #{UI.cyan(verify_url)}")
|
|
@@ -48,9 +56,15 @@ module Everywhere
|
|
|
48
56
|
sleep(interval)
|
|
49
57
|
UI.die!("login timed out — run `every platform login` again") if monotonic > deadline
|
|
50
58
|
|
|
51
|
-
|
|
59
|
+
# The user is mid-approval in a browser; a dropped connection here must
|
|
60
|
+
# not kill the login. Keep polling until the deadline instead.
|
|
61
|
+
res = begin
|
|
62
|
+
client.post("/cli/device_authorizations/token", device_code: device_code)
|
|
63
|
+
rescue Everywhere::Error
|
|
64
|
+
next
|
|
65
|
+
end
|
|
52
66
|
UI.die!("login failed — #{res.body["error"] || "invalid request"}") if res.status == 400
|
|
53
|
-
next unless res.ok? # transient 5xx
|
|
67
|
+
next unless res.ok? # transient 5xx: keep polling until the deadline
|
|
54
68
|
|
|
55
69
|
case res.body["status"]
|
|
56
70
|
when "approved" then return res.body
|
|
@@ -7,15 +7,16 @@ require "shellwords"
|
|
|
7
7
|
require "base64"
|
|
8
8
|
require "json"
|
|
9
9
|
require "open3"
|
|
10
|
+
require "securerandom"
|
|
10
11
|
require_relative "../../ui"
|
|
11
12
|
require_relative "../../paths"
|
|
12
13
|
require_relative "../../platform/client"
|
|
13
14
|
|
|
14
15
|
module Everywhere
|
|
15
16
|
module Commands
|
|
16
|
-
# `every platform runner` — runs ON a build runner
|
|
17
|
-
#
|
|
18
|
-
# `every release` signing engine:
|
|
17
|
+
# `every platform runner` — runs ON a build runner: a macOS machine for the
|
|
18
|
+
# macos/ios targets, macOS or Linux for android. It's the glue between the
|
|
19
|
+
# Runner API and the `every release` signing engine:
|
|
19
20
|
#
|
|
20
21
|
# job spec → mark running → download+extract snapshot → pull JIT creds into
|
|
21
22
|
# a throwaway keychain (+ .p8) → every release (streaming logs) → upload the
|
|
@@ -24,6 +25,28 @@ module Everywhere
|
|
|
24
25
|
# Authenticated with the per-artifact runner token, so it can only touch the
|
|
25
26
|
# one artifact it was dispatched for.
|
|
26
27
|
class PlatformRunner < Dry::CLI::Command
|
|
28
|
+
# What `every release` leaves in dist/ per os, newest first when several
|
|
29
|
+
# match, and how the blob store should label it.
|
|
30
|
+
ARTIFACTS = { "macos" => %w[*.zip], "ios" => %w[*.ipa], "android" => %w[*.aab *.apk] }.freeze
|
|
31
|
+
CONTENT_TYPES = {
|
|
32
|
+
".zip" => "application/zip",
|
|
33
|
+
".ipa" => "application/octet-stream",
|
|
34
|
+
".apk" => "application/vnd.android.package-archive",
|
|
35
|
+
".aab" => "application/octet-stream"
|
|
36
|
+
}.freeze
|
|
37
|
+
RELEASE_NARRATION = {
|
|
38
|
+
"macos" => "every release — press → sign → notarize → staple",
|
|
39
|
+
"ios" => "every release — build → archive → export .ipa",
|
|
40
|
+
"android" => "every release — assemble → sign → bundle"
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
# A .mobileprovision has to be in both: Xcode reads the UserData copy,
|
|
44
|
+
# xcodebuild's own resolution still reads the MobileDevice one.
|
|
45
|
+
PROFILE_DIRS = [
|
|
46
|
+
"~/Library/MobileDevice/Provisioning Profiles",
|
|
47
|
+
"~/Library/Developer/Xcode/UserData/Provisioning Profiles"
|
|
48
|
+
].freeze
|
|
49
|
+
|
|
27
50
|
desc "Run a Platform build on this machine (used by the hosted runner)"
|
|
28
51
|
|
|
29
52
|
option :artifact, required: true, desc: "Artifact public id (art_…)"
|
|
@@ -49,10 +72,10 @@ module Everywhere
|
|
|
49
72
|
src = fetch_snapshot(work, job)
|
|
50
73
|
signing_env = pull_credentials(work)
|
|
51
74
|
|
|
52
|
-
status, release_json,
|
|
75
|
+
status, release_json, built = run_release(src, job, signing_env, shell_dir)
|
|
53
76
|
|
|
54
|
-
if status == "succeeded" &&
|
|
55
|
-
complete_success(
|
|
77
|
+
if status == "succeeded" && built
|
|
78
|
+
complete_success(built, release_json)
|
|
56
79
|
else
|
|
57
80
|
complete_failure("build failed — see log")
|
|
58
81
|
end
|
|
@@ -62,6 +85,12 @@ module Everywhere
|
|
|
62
85
|
rescue Everywhere::Error => e
|
|
63
86
|
complete_failure(e.message)
|
|
64
87
|
UI.die!(e.message)
|
|
88
|
+
rescue Everywhere::Fatal => e
|
|
89
|
+
# UI.die! raises Fatal (a SystemExit), which the rescue above can't see —
|
|
90
|
+
# without this an upload/signing failure would leave the artifact stuck
|
|
91
|
+
# "running" on the Platform forever.
|
|
92
|
+
complete_failure(e.message.to_s.empty? ? "build failed" : e.message)
|
|
93
|
+
raise
|
|
65
94
|
end
|
|
66
95
|
|
|
67
96
|
private
|
|
@@ -86,18 +115,64 @@ module Everywhere
|
|
|
86
115
|
path = File.join(work, cred["filename"])
|
|
87
116
|
File.binwrite(path, Base64.strict_decode64(cred["data_base64"]))
|
|
88
117
|
File.chmod(0o600, path)
|
|
118
|
+
meta = cred["metadata"] || {}
|
|
89
119
|
case cred["kind"]
|
|
90
120
|
when "apple_developer_id", "apple_distribution"
|
|
91
|
-
env["
|
|
121
|
+
env["EVERY_IOS_TEAM_ID"] ||= meta["team_id"]
|
|
122
|
+
# An android job runs on Linux, where there's no keychain to import
|
|
123
|
+
# into. A stray apple credential there is the Platform's mistake, not
|
|
124
|
+
# a reason to lose the build.
|
|
125
|
+
if darwin?
|
|
126
|
+
env["APPLE_SIGNING_IDENTITY"] = install_certificate(path, meta, work)
|
|
127
|
+
else
|
|
128
|
+
remote_log("skipping #{cred["kind"]} — keychain import needs macOS")
|
|
129
|
+
end
|
|
92
130
|
when "apple_asc_api_key"
|
|
93
131
|
env["NOTARY_KEY"] = path
|
|
94
|
-
env["NOTARY_KEY_ID"] =
|
|
95
|
-
env["NOTARY_ISSUER"] =
|
|
132
|
+
env["NOTARY_KEY_ID"] = meta["key_id"]
|
|
133
|
+
env["NOTARY_ISSUER"] = meta["issuer_id"]
|
|
134
|
+
when "apple_provisioning_profile"
|
|
135
|
+
profile = install_provisioning_profile(path)
|
|
136
|
+
env["EVERY_IOS_PROVISIONING_PROFILE"] = profile[:name]
|
|
137
|
+
env["EVERY_IOS_TEAM_ID"] ||= profile[:team]
|
|
138
|
+
when "android_keystore"
|
|
139
|
+
env["EVERY_ANDROID_KEYSTORE"] = path
|
|
140
|
+
env["EVERY_ANDROID_KEYSTORE_PASSWORD"] = meta["store_password"]
|
|
141
|
+
env["EVERY_ANDROID_KEY_ALIAS"] = meta["key_alias"]
|
|
142
|
+
# One-key keystores are usually created with a single password.
|
|
143
|
+
env["EVERY_ANDROID_KEY_PASSWORD"] = meta["key_password"] || meta["store_password"]
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
env.compact
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# A .mobileprovision is a CMS-signed DER blob wrapped around a plain XML
|
|
150
|
+
# plist. Scanning for the plist reads it identically on Linux, where
|
|
151
|
+
# `security cms -D` doesn't exist.
|
|
152
|
+
def install_provisioning_profile(path)
|
|
153
|
+
plist = File.binread(path)[/<\?xml.*<\/plist>/m]
|
|
154
|
+
UI.die!("the delivered provisioning profile has no embedded plist") unless plist
|
|
155
|
+
|
|
156
|
+
uuid = plist[%r{<key>UUID</key>\s*<string>([^<]+)</string>}m, 1]
|
|
157
|
+
name = plist[%r{<key>Name</key>\s*<string>([^<]+)</string>}m, 1]
|
|
158
|
+
# TeamIdentifier is an <array> of one — the first <string> after the key.
|
|
159
|
+
team = plist[%r{<key>TeamIdentifier</key>.*?<string>([^<]+)</string>}m, 1]
|
|
160
|
+
UI.die!("the delivered provisioning profile has no UUID") unless uuid
|
|
161
|
+
|
|
162
|
+
if darwin?
|
|
163
|
+
profile_dirs.each do |dir|
|
|
164
|
+
FileUtils.mkdir_p(dir)
|
|
165
|
+
FileUtils.cp(path, File.join(dir, "#{uuid}.mobileprovision"))
|
|
96
166
|
end
|
|
97
167
|
end
|
|
98
|
-
|
|
168
|
+
remote_log("provisioning profile #{UI.bold(name || uuid)}#{team ? " #{UI.dim("(team #{team})")}" : ""}")
|
|
169
|
+
{ name: name, team: team, uuid: uuid }
|
|
99
170
|
end
|
|
100
171
|
|
|
172
|
+
def profile_dirs = PROFILE_DIRS.map { |dir| File.expand_path(dir) }
|
|
173
|
+
|
|
174
|
+
def darwin? = RUBY_PLATFORM.include?("darwin")
|
|
175
|
+
|
|
101
176
|
# Import the .p12 into a throwaway keychain and add it to the search list so
|
|
102
177
|
# codesign finds the identity. Returns the identity name for notarize.sh.
|
|
103
178
|
def install_certificate(p12, metadata, work)
|
|
@@ -124,11 +199,16 @@ module Everywhere
|
|
|
124
199
|
end
|
|
125
200
|
|
|
126
201
|
def run_release(src, job, signing_env, shell_dir)
|
|
127
|
-
|
|
202
|
+
os = job.dig("target", "os").to_s
|
|
203
|
+
channel = job.dig("target", "distribution_channel").to_s
|
|
204
|
+
remote_log(RELEASE_NARRATION.fetch(os, "every release"))
|
|
128
205
|
cmd = [RbConfig.ruby, "-I", lib_dir, every_exe, "release",
|
|
129
206
|
"--root", src,
|
|
130
|
-
"--target", "#{
|
|
131
|
-
"--channel",
|
|
207
|
+
"--target", "#{os}-#{job.dig("target", "arch")}",
|
|
208
|
+
"--channel", channel]
|
|
209
|
+
# Play wants an App Bundle; every other Android channel (direct download,
|
|
210
|
+
# sideload, Amazon) wants the installable apk `every release` defaults to.
|
|
211
|
+
cmd += ["--format", "aab"] if os == "android" && channel == "play"
|
|
132
212
|
cmd += ["--shell-dir", shell_dir] if shell_dir
|
|
133
213
|
|
|
134
214
|
# The web UI renders ANSI, but `every release` runs here as a pipe (not a
|
|
@@ -144,37 +224,45 @@ module Everywhere
|
|
|
144
224
|
|
|
145
225
|
dist = File.join(src, "dist")
|
|
146
226
|
release_json = JSON.parse(File.read(File.join(dist, "release.json"))) rescue nil
|
|
147
|
-
|
|
148
|
-
|
|
227
|
+
built = ARTIFACTS.fetch(os, %w[*.zip])
|
|
228
|
+
.filter_map { |p| Dir[File.join(dist, p)].max_by { |f| File.mtime(f) } }.first
|
|
229
|
+
[success ? "succeeded" : "failed", release_json, built]
|
|
149
230
|
end
|
|
150
231
|
|
|
151
|
-
def complete_success(
|
|
232
|
+
def complete_success(built, release_json)
|
|
152
233
|
report_phase("uploading")
|
|
153
|
-
remote_log("uploading artifact #{UI.dim("(#{(File.size(
|
|
154
|
-
signed_id = upload_artifact(
|
|
234
|
+
remote_log("uploading artifact #{UI.dim("(#{(File.size(built) / 1024.0 / 1024.0).round(1)}MB)")}")
|
|
235
|
+
signed_id = upload_artifact(built)
|
|
155
236
|
post!("/runner/artifacts/#{@artifact}/complete",
|
|
156
237
|
status: "succeeded", file_signed_id: signed_id, release_json: release_json)
|
|
157
|
-
UI.success("build succeeded — #{File.basename(
|
|
238
|
+
UI.success("build succeeded — #{File.basename(built)}")
|
|
158
239
|
end
|
|
159
240
|
|
|
241
|
+
# Best-effort: if the thing that failed WAS connectivity, reporting the
|
|
242
|
+
# failure fails too — raising here would replace the real error and abort
|
|
243
|
+
# cleanup, so log locally and move on.
|
|
160
244
|
def complete_failure(message)
|
|
161
245
|
return if @completed
|
|
162
246
|
|
|
163
247
|
@client.post("/runner/artifacts/#{@artifact}/complete", status: "failed", error: message)
|
|
164
248
|
@completed = true
|
|
249
|
+
rescue Everywhere::Error => e
|
|
250
|
+
UI.warn("couldn't report the failure to the Platform (#{e.message})")
|
|
165
251
|
end
|
|
166
252
|
|
|
167
|
-
def upload_artifact(
|
|
253
|
+
def upload_artifact(built)
|
|
168
254
|
require_relative "../../platform/snapshot"
|
|
169
255
|
reservation = post!("/runner/artifacts/#{@artifact}/upload",
|
|
170
|
-
filename: File.basename(
|
|
171
|
-
checksum: Everywhere::Platform::Snapshot.md5_base64(
|
|
256
|
+
filename: File.basename(built), byte_size: File.size(built),
|
|
257
|
+
checksum: Everywhere::Platform::Snapshot.md5_base64(built), content_type: content_type_for(built))
|
|
172
258
|
du = reservation["direct_upload"] || {}
|
|
173
|
-
res = @client.put_file(du["url"],
|
|
259
|
+
res = @client.put_file(du["url"], built, headers: du["headers"] || {})
|
|
174
260
|
UI.die!("artifact upload failed (HTTP #{res.code})") unless res.code.to_i.between?(200, 299)
|
|
175
261
|
reservation["signed_id"]
|
|
176
262
|
end
|
|
177
263
|
|
|
264
|
+
def content_type_for(path) = CONTENT_TYPES.fetch(File.extname(path), "application/octet-stream")
|
|
265
|
+
|
|
178
266
|
# ---- log streaming -------------------------------------------------------
|
|
179
267
|
|
|
180
268
|
# Run cmd, echoing output to this process's stdout AND forwarding it to the
|
|
@@ -241,6 +329,8 @@ module Everywhere
|
|
|
241
329
|
end
|
|
242
330
|
|
|
243
331
|
def restore_keychains
|
|
332
|
+
return unless darwin?
|
|
333
|
+
|
|
244
334
|
run_security("list-keychains", "-d", "user", "-s", *@saved_keychains) if @saved_keychains
|
|
245
335
|
run_security("delete-keychain", @keychain) if @keychain && File.exist?(@keychain)
|
|
246
336
|
end
|