Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,14 @@ the device your Mac controls, install links must be opened on that Mac —
scanning a QR code with a phone does not work with Tophat.

**First-time setup.** `dev up` installs Tophat and seeds Quick Launch entries.
Tophat needs a Bitrise Personal Access Token to download build artifacts:

1. Create a PAT at https://app.bitrise.io/me/account/security
2. Add it in Tophat -> Settings -> Extensions -> Bitrise
Installs need a Bitrise Personal Access Token to download build artifacts. The
first time you run `dev tophat` and no token is stored, it opens the Bitrise
token page, prompts you to paste a token (input hidden), stores it in your
login keychain, and saves it into Tophat for you (macOS may ask to let
`security` update Tophat's keychain entry — choose Always Allow). If you would
rather configure it yourself, create a PAT at
https://app.bitrise.io/me/account/security and add it in
Tophat -> Settings -> Extensions -> Bitrise.

There are three ways to install a build:

Expand All @@ -101,10 +105,18 @@ There are three ways to install a build:
dev tophat https://github.com/Shopify/checkout-kit/pull/382
```

It resolves the PR's branch, asks what to test (e.g. React Native iOS /
Android), reuses a running device that matches or lets you pick one with
`fzf`, then installs the matching artifact. Set `TOPHAT_DRY_RUN=1` to print
the generated install config without installing.
It resolves the PR's branch, then queries Bitrise for that branch and only
offers targets whose latest build succeeded and produced the expected
artifact, so you cannot pick a target that has no installable build. If no
target is ready it explains why (draft PRs do not trigger the pipeline, a
build is still running, or a build failed) and exits. Pass `--wait` to block
until in-progress builds finish, then install. It then asks what to test
(e.g. React Native iOS / Android), reuses a running device that matches or
lets you pick one with `fzf`, and installs the matching artifact.

Set `TOPHAT_DRY_RUN=1` to print the generated install config without
installing, or `TOPHAT_SKIP_ARTIFACT_CHECK=1` to skip the Bitrise artifact
filtering and offer every target.

**Adding a new SDK target.** Add an entry to `scripts/tophat/targets.json` with
an `id`, `label`, and `recipes` (each a `platform`, `destination`, Bitrise
Expand Down
124 changes: 124 additions & 0 deletions scripts/tophat/bitrise_auth.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# frozen_string_literal: true

require "io/console"
require "open3"
require_relative "common"
require_relative "bitrise_client"

module BitriseAuth
KEYCHAIN_SERVICE = "com.shopify.checkout-kit.tophat"
KEYCHAIN_ACCOUNT = "bitrise-pat"
TOPHAT_KEYCHAIN_SERVICE = "com.shopify.Tophat.TophatBitriseExtension"
TOPHAT_KEYCHAIN_ACCOUNT = "TophatBitrisePersonalAccessToken"
TOPHAT_APP = "/Applications/Tophat.app"
module_function

def resolve_token(app_slug)
stored = read_keychain(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT)
return stored if stored && token_status(stored, app_slug) != :unauthorized

bootstrap_token(app_slug)
end

def clear_token
system("security", "delete-generic-password", "-s", KEYCHAIN_SERVICE, "-a", KEYCHAIN_ACCOUNT, out: File::NULL, err: File::NULL)
end

def ensure_tophat_token(app_slug)
token = read_keychain(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT)
return false unless token && token_status(token, app_slug) == :ok

puts "==> Your saved Bitrise token is authorized, but Tophat's token is missing or invalid; syncing them."
sync_to_tophat(token)
true
end

def token_status(token, app_slug)
BitriseClient.new(token: token, app_slug: app_slug).authorized? ? :ok : :unauthorized
rescue
:unknown
end

def bootstrap_token(app_slug)
open_token_page
token = prompt_until_valid(app_slug)
store_token(token)
sync_to_tophat(token)
token
end

def open_token_page
puts <<~MESSAGE

A Bitrise Personal Access Token is required to check builds and download artifacts.
Opening the Bitrise token page. Create a token, then paste it below.

MESSAGE
system("open", CheckoutKitTophat::BITRISE_PAT_URL)
end

def prompt_until_valid(app_slug)
loop do
token = prompt_token
CheckoutKitTophat.abort_with("Cancelled.") if token.nil? || token.empty?

case token_status(token, app_slug)
when :ok
puts "✅ Authentication successful."
return token
when :unknown
warn "Could not reach Bitrise to validate the token; continuing."
return token
else
warn "Bitrise rejected that token. Check it was copied correctly and try again, or press Ctrl-C to cancel."
end
end
end

def prompt_token
print "Paste your Bitrise Personal Access Token (input hidden): "
token = STDIN.tty? ? STDIN.noecho(&:gets) : STDIN.gets
puts
token&.strip
end

def store_token(token)
write_keychain(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, token) ||
CheckoutKitTophat.abort_with("Could not store the Bitrise token in your keychain.")
end

def sync_to_tophat(token)
return true unless File.exist?(TOPHAT_APP)

puts <<~MESSAGE
Storing the token in Tophat for the Bitrise artifact provider.
🔑 macOS may ask for your login password to let Tophat read this token
(Recommended) Approve with 'Always Allow' so the install can in future.
MESSAGE
system("security", "delete-generic-password", "-s", TOPHAT_KEYCHAIN_SERVICE, "-a", TOPHAT_KEYCHAIN_ACCOUNT, out: File::NULL, err: File::NULL)
if write_keychain(TOPHAT_KEYCHAIN_SERVICE, TOPHAT_KEYCHAIN_ACCOUNT, token, trusted_app: TOPHAT_APP)
puts "✅ Tophat is configured with your Bitrise token."
return true
end

warn <<~MESSAGE
Could not save the token into Tophat automatically.
Add it manually in Tophat -> Settings -> Extensions -> Bitrise before installing.
MESSAGE
false
end

def read_keychain(service, account)
stdout, status = Open3.capture2e("security", "find-generic-password", "-s", service, "-a", account, "-w")
return nil unless status.success?

value = stdout.strip
value.empty? ? nil : value
end

def write_keychain(service, account, token, trusted_app: nil)
args = ["security", "add-generic-password", "-U", "-s", service, "-a", account, "-w", token]
args += ["-T", trusted_app] if trusted_app
system(*args, out: File::NULL, err: File::NULL)
end
end
43 changes: 43 additions & 0 deletions scripts/tophat/bitrise_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

require "json"
require "net/http"
require "uri"

class BitriseClient
API_HOST = "api.bitrise.io"
API_BASE = "/v0.1"

def initialize(token:, app_slug:)
@token = token
@app_slug = app_slug
end

def authorized?
get("#{API_BASE}/me").is_a?(Net::HTTPSuccess)
end

def latest_build(branch:, workflow:)
query = URI.encode_www_form(branch: branch, workflow: workflow, limit: 1)
get_json("#{API_BASE}/apps/#{@app_slug}/builds?#{query}").fetch("data", []).first
end

def artifact_titles(build_slug)
path = "#{API_BASE}/apps/#{@app_slug}/builds/#{build_slug}/artifacts"
get_json(path).fetch("data", []).map { |artifact| artifact.fetch("title") }
end

private

def get_json(path)
response = get(path)
raise "Bitrise request failed #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
JSON.parse(response.body)
end

def get(path)
request = Net::HTTP::Get.new(path)
request["Authorization"] = @token
Net::HTTP.start(API_HOST, 443, use_ssl: true) { |http| http.request(request) }
end
end
4 changes: 3 additions & 1 deletion scripts/tophat/configure_quick_launch
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ end
puts <<~REMINDER

==> Tophat Quick Launch items configured for Checkout Kit.
Installs need a Bitrise Personal Access Token:
Installs need a Bitrise Personal Access Token. The first run of
'dev tophat' sets one up for you (it prompts for a token and saves it
into Tophat). To configure it manually instead:
1. Create a PAT at #{CheckoutKitTophat::BITRISE_PAT_URL}
2. Add it to Tophat -> Settings -> Extensions -> Bitrise
REMINDER
Loading
Loading