Skip to content

Commit c346dc8

Browse files
Soft-launch E2E PR checks
Assisted-By: devx/396ca0c4-44ad-415d-8d80-7200df4e42a4
1 parent 3545355 commit c346dc8

5 files changed

Lines changed: 149 additions & 9 deletions

File tree

e2e/BITRISE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ Green runs update statuses and Check Runs without creating new PR comments. Fail
8787

8888
## PR trigger
8989

90-
Configure the Bitrise app to run the `e2e` pipeline for pull requests once the skeleton is ready to validate in Bitrise.
90+
`bitrise.yml` maps pull requests to the `e2e` pipeline with `trigger_map`. Keep the GitHub checks non-required while `E2E_STRICT=false`.
9191

9292
## Code signing
9393

e2e/RUNBOOK.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Checkout Kit E2E Runbook
2+
3+
## Rollout mode
4+
5+
The Bitrise E2E pipeline starts in soft-fail mode:
6+
7+
```bash
8+
E2E_STRICT=false
9+
```
10+
11+
In soft-fail mode, BrowserStack Maestro assertion failures are reported through GitHub commit statuses, Check Runs, and the sticky PR failure comment, but the BrowserStack workflow exits successfully so the initial rollout does not block PR progress.
12+
13+
Set `E2E_STRICT=true` only after the suite has stabilized and the corresponding GitHub contexts are ready to become required checks.
14+
15+
## Retry behavior
16+
17+
BrowserStack API calls retry transient infrastructure responses once by default:
18+
19+
```bash
20+
E2E_BROWSERSTACK_API_RETRIES=1
21+
```
22+
23+
Retry applies to HTTP 429 and 5xx responses. Maestro assertion failures are not auto-retried by default so first-failure evidence is preserved.
24+
25+
## Timeouts
26+
27+
BrowserStack polling uses these defaults:
28+
29+
```bash
30+
E2E_BROWSERSTACK_TIMEOUT_SECONDS=1800
31+
E2E_BROWSERSTACK_POLL_SECONDS=30
32+
```
33+
34+
If polling times out, the runner attempts to stop the BrowserStack build before failing or soft-failing according to `E2E_STRICT`.
35+
36+
## Local rerun notes
37+
38+
Use the matrix row from a failure report to identify the app target, platform, OS track, and suite:
39+
40+
```bash
41+
ruby e2e/scripts/e2e_matrix expand --index <index>
42+
```
43+
44+
Use the reported resolved device to pin a rerun:
45+
46+
```bash
47+
E2E_DEVICE_OVERRIDE="<resolved BrowserStack device>" \
48+
E2E_STRICT=true \
49+
e2e/scripts/browserstack_maestro --index <index> --suite-zip <suite.zip> --output-dir <results-dir>
50+
```
51+
52+
The app artifact environment variable for the matrix row must point at the `.apk` or `.ipa` artifact before rerunning.
53+
54+
## Failure triage
55+
56+
Use the GitHub Check Run or sticky PR failure comment first. Failure summaries should include direct links to:
57+
58+
- BrowserStack build
59+
- failed testcase
60+
- video
61+
- screenshot
62+
- Maestro command log
63+
- Maestro log
64+
- device log
65+
- network log when enabled
66+
67+
Avoid posting additional PR comments for green runs.

e2e/bitrise.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ app:
1616
- E2E_IOS_EXPORT_METHOD: development
1717
- RBENV_VERSION: system
1818

19+
trigger_map:
20+
- pull_request_source_branch: "*"
21+
pipeline: e2e
22+
1923
pipelines:
2024
e2e:
2125
workflows:
@@ -176,7 +180,7 @@ workflows:
176180
results_dir="$BITRISE_DEPLOY_DIR/e2e/results/run-${run_index}"
177181
mkdir -p "$results_dir"
178182
envman add --key E2E_BROWSERSTACK_RESULTS_DIR --value "$results_dir"
179-
e2e/scripts/browserstack_maestro \
183+
E2E_STRICT="${E2E_STRICT:-false}" e2e/scripts/browserstack_maestro \
180184
--index "$run_index" \
181185
--suite-zip "$E2E_MAESTRO_SUITE_ZIP" \
182186
--output-dir "$results_dir"

e2e/scripts/browserstack_maestro

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,15 @@ class BrowserStackMaestro
3333
sessions = fetch_sessions(build_status)
3434
result = normalize_result(run, device, app, suite, build, build_status, sessions)
3535
write_json("result.json", result)
36-
exit(result.fetch("passed") ? 0 : 1)
36+
exit(result.fetch("passed") || !strict? ? 0 : 1)
3737
end
3838

3939
private
4040

41+
def strict?
42+
ENV.fetch("E2E_STRICT", "false") == "true"
43+
end
44+
4145
def resolve_device(run)
4246
override = ENV["E2E_DEVICE_OVERRIDE"] || ENV["E2E_#{run.fetch("platform").upcase}_DEVICE_OVERRIDE"]
4347
if override && !override.empty?
@@ -85,12 +89,21 @@ class BrowserStackMaestro
8589
response = get_json("/app-automate/maestro/v2/builds/#{build_id}")
8690
write_json("build-status.json", response)
8791
return response if TERMINAL_STATUSES.include?(response.fetch("status").to_s.downcase)
88-
raise "BrowserStack build timed out: #{build_id}" if Time.now >= deadline
92+
if Time.now >= deadline
93+
stop_build(build_id)
94+
raise "BrowserStack build timed out: #{build_id}"
95+
end
8996

9097
sleep ENV.fetch("E2E_BROWSERSTACK_POLL_SECONDS", "30").to_i
9198
end
9299
end
93100

101+
def stop_build(build_id)
102+
post_json("/app-automate/maestro/builds/#{build_id}/stop", {})
103+
rescue StandardError => error
104+
warn "Unable to stop BrowserStack build #{build_id}: #{error.message}"
105+
end
106+
94107
def fetch_sessions(build_status)
95108
build_id = build_status.fetch("id")
96109
build_status.fetch("devices", []).flat_map do |device|
@@ -155,12 +168,28 @@ class BrowserStackMaestro
155168
end
156169

157170
def execute_request(request)
158-
request.basic_auth(@username, @access_key)
159-
response = Net::HTTP.start(API_HOST, 443, use_ssl: true) { |http| http.request(request) }
160-
parsed = JSON.parse(response.body)
161-
return parsed if response.is_a?(Net::HTTPSuccess)
171+
attempts = 0
172+
retries = ENV.fetch("E2E_BROWSERSTACK_API_RETRIES", "1").to_i
173+
loop do
174+
attempts += 1
175+
request.basic_auth(@username, @access_key)
176+
response = Net::HTTP.start(API_HOST, 443, use_ssl: true) { |http| http.request(request) }
177+
parsed = parse_response_body(response)
178+
return parsed if response.is_a?(Net::HTTPSuccess)
179+
raise "BrowserStack request failed #{response.code}: #{parsed}" unless retryable_browserstack_response?(response) && attempts <= retries
180+
181+
sleep attempts
182+
end
183+
end
184+
185+
def parse_response_body(response)
186+
JSON.parse(response.body)
187+
rescue JSON::ParserError
188+
raise "BrowserStack request returned non-JSON response"
189+
end
162190

163-
raise "BrowserStack request failed #{response.code}: #{parsed}"
191+
def retryable_browserstack_response?(response)
192+
response.code.to_i == 429 || response.code.to_i >= 500
164193
end
165194

166195
def custom_id(run, suffix)

e2e/test/soft_launch_test.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# frozen_string_literal: true
2+
3+
require "minitest/autorun"
4+
require "yaml"
5+
6+
class E2ESoftLaunchTest < Minitest::Test
7+
BITRISE_CONFIG_PATH = "e2e/bitrise.yml"
8+
9+
def test_bitrise_pr_trigger_runs_e2e_pipeline
10+
config = YAML.safe_load_file(BITRISE_CONFIG_PATH)
11+
trigger = config.fetch("trigger_map").find { |entry| entry["pull_request_source_branch"] == "*" }
12+
13+
assert_equal "e2e", trigger.fetch("pipeline")
14+
end
15+
16+
def test_browserstack_runner_supports_soft_fail_and_retry_controls
17+
script = File.read("e2e/scripts/browserstack_maestro")
18+
19+
assert_includes script, "E2E_STRICT"
20+
assert_includes script, "E2E_BROWSERSTACK_API_RETRIES"
21+
assert_includes script, "retryable_browserstack_response?"
22+
assert_includes script, '/app-automate/maestro/builds/#{build_id}/stop'
23+
end
24+
25+
def test_bitrise_browserstack_workflow_defaults_to_soft_fail
26+
config = YAML.safe_load_file(BITRISE_CONFIG_PATH)
27+
run_script = config.fetch("workflows").fetch("e2e-run-browserstack").fetch("steps").flat_map do |step|
28+
step_value = step.values.first
29+
next [] unless step.keys.first.start_with?("script")
30+
31+
step_value.fetch("inputs", []).filter_map { |input| input["content"] }
32+
end.join("\n")
33+
34+
assert_includes run_script, "E2E_STRICT=\"${E2E_STRICT:-false}\""
35+
end
36+
37+
def test_runbook_exists
38+
assert File.exist?("e2e/RUNBOOK.md")
39+
end
40+
end

0 commit comments

Comments
 (0)