ci: add E2E staging workflow for iOS - #747
Conversation
Add .github/workflows/e2e-staging.yml that runs staging E2E matrix separately from the main e2e-tests.yml merge gate. Triggers: - schedule: 04:00 UTC daily (after migration cron) - workflow_dispatch: with e2e_branch input for custom branch - pull_request: for self-testing this PR Staging shards: - @multi_address_2 - @pubky - @transfer_1|@transfer_max Features: - Reuses build-staging and e2e-branch patterns from e2e-tests.yml - BACKEND=regtest for staging environment - 3 retry attempts with simulator reset before attempt 3 - Slack failure notification to #bitkit-staging-nightly Refs: synonymdev/bitkit-e2e-tests#221 Co-authored-by: piotr-iohk <piotr-iohk@users.noreply.github.com>
|
- P1: notify-failure now depends on build-staging, e2e-branch, and e2e-tests-staging; fires if any fails on schedule/workflow_dispatch - P2: pin slackapi/slack-github-action to SHA b0fa283ad8fea605de13dc3f449259339835fc52 (v2.1.0) Co-authored-by: piotr-iohk <piotr-iohk@users.noreply.github.com>
…taging - Remove pull_request trigger (keep only schedule and workflow_dispatch) - Remove transfer_staging shard (@transfer_1|@transfer_max) from matrix - Remove draft PR checks (no longer needed without PR trigger) - Simplify notify-failure condition (keep build-staging/e2e-branch/e2e-tests-staging coverage) Transfers deferred to PR B. Co-authored-by: piotr-iohk <piotr-iohk@users.noreply.github.com>
Add @transfer_max grep to staging workflow. @transfer_1 temporarily omitted pending e2e-tests fix (PR B). Co-authored-by: piotr-iohk <piotr-iohk@users.noreply.github.com>
- Add boolean input notify_slack (default: false) - Scheduled runs: always notify Slack on failure - Manual dispatch: notify only when checkbox is checked Co-authored-by: piotr-iohk <piotr-iohk@users.noreply.github.com>
There was a problem hiding this comment.
Reviewed 1 file: Adds a nightly and manually dispatched staging E2E workflow that builds a regtest iOS app, runs three shard greps with three attempts each, and posts a Slack alert when a shard fails. 1 non-blocking finding inline.
Coverage
Total: 0% (whole PR, 1 file)
- Journeys: 0% - The change is a CI workflow; no user journey is implemented, changed, or exercised by it.
- Unit tests: 0% - No unit or integration suite covers
e2e-staging.yml, and the diff adds no test. - QA: 0% - Manual Tests not run
Reviewed by Codex (deepseek-v4.1-flash-high) via gh-pr-review-loop skill
jvsena42
left a comment
There was a problem hiding this comment.
No findings. This is a faithful merge of two workflows that already work in this repo, and I verified the merge mechanically rather than by eye: build-staging against e2e-tests.yml:134-222, and e2e-tests-staging against e2e-tests.yml:373-508. They differ only in dropped detect-changes gating, the shard list, BACKEND: regtest hoisted to job level (line 136, semantically identical), hardcoded strings replaced by env refs, and artifact names (-staging_ vs -regtest_, consistent between the upload at line 111 and the download at line 154).
What I checked that was worth checking:
- Staging really is staging.
Env.swift:19-25makesE2E_BUILDsetisE2E = true;:71readsE2E_BACKENDfrom Info.plist (Info.plist:22-23maps$(E2E_BACKEND));:95readsE2E_NETWORK. WithE2E_BACKEND: network+E2E_NETWORK: regtest(lines 81-82),electrumServerUrlfalls through toelectrumServerUrl(for: .regtest)=ssl://electrs.bitkit.stag0.blocktank.to:9999— staging, not the local docker electrum.E2E_BUILDat line 99 is correct here, and dropping it would be the bug:isE2Ewould go false, disabling the E2E launch hooks while still landing on the same electrum. That line is byte-identical to the sibling's ownbuild-stagingjob (e2e-tests.yml:207). - Failure propagation is sound. Lines 204/215 put
continue-on-error: trueon attempts 1-2; attempt 3 (lines 238-247) has none, so its failure fails the job. All four paths traced: pass-first → green; fail-then-pass → green; all-three-fail → red →needs.e2e-tests-staging.result == 'failure'→ Slack fires. The.outcomereads at 216/227/239 are the correct accessor undercontinue-on-error(.conclusionwould be wrong). - The Slack pin is a real release. Line 269's
b0fa283ad8fea605de13dc3f449259339835fc52resolves to tagv2.1.0. SLACK_WEBHOOK_URL_STAGINGexists as a repo secret here, so line 271 will not fail with "Missing input".- The nightly will actually fire. The default branch is
master, ande2e_migration.yml:10-11already proves aschedule:nightly runs in this repo.0 4 * * *(line 5) does not collide with it (0 3 * * *). - No injection surface. The only
${{ }}values reaching arun:shell arematrix.shard.grep(workflow-literal) andenv.SIMULATOR_NAME. The dispatch inpute2e_branchreaches only a reusable-workflowwith:and a checkoutref:, never a shell;needs.e2e-branch.outputs.branchgoes through anenv:indirection (lines 140-142), the safe form. - The
${{ env.SIMULATOR_NAME }}self-reference in a stepenv:block (lines 209-210) looks suspicious but is the established in-production pattern here, identical toe2e_migration.yml:282-283.
Parity against the Android twin synonymdev/bitkit-android#1253, which I reviewed in the same pass:
| Behaviour | iOS | Android | Verdict |
|---|---|---|---|
| Schedule | 0 4 * * * UTC (L5) |
same (L5) | Same — both alert at the same wall-clock time |
| Concurrency | cancel-in-progress: true on a default-branch-only group (L25-27) |
same (L22-24) | Same; I raised the dispatch-kills-the-nightly case on the Android side and it applies equally here |
| Failure propagation | attempt 3 not continue-on-error (L238-247) |
same (L208-223) | Same |
| Notify condition | result == 'failure' (L257-263) |
same, reordered (L237-243) | Same shape — both miss cancelled/timed_out, already open on this PR |
| Permissions | absent | absent | Same, and also absent in every existing sibling in both repos |
| Extra failure diagnostics | absent | jwalton/gh-docker-logs@v2 (L232-234) |
Correctly absent here — staging uses the network backend, no docker |
| Job timeout | timeout-minutes: 90 (L122) |
absent (default 360) | Real reporting divergence — a hung Android shard delays the Slack alert by up to 6h vs 90min here. Raised on the Android PR; nothing to change on this one |
| Slack action pin | b0fa283… = v2.1.0 (L269) |
e598089… = v2.0.0 (L248) |
Differently shaped; one side should move |
| Slack payload | JSON with workflow/branch/trigger (L273-285) | YAML, run link only (L252-258) | Differently shaped — the same channel will get two differently-shaped alerts |
| Shards | multi_address_2, pubky, transfer_max |
those plus hardware_wallet |
Expected |
Not re-raised, already on record here: the prerequisite-failure alert gap, the mutable Slack action tag (now pinned), and the cancelled-result notification gap under cancel-in-progress: true.
- Replace notify-failure job with slack-report using shared
scripts/slack_summary.py from bitkit-e2e-tests
- Rename input notify_slack → post_to_slack
- Change concurrency to cancel-in-progress: ${{ github.event_name != 'schedule' }}
so dispatch cannot silently kill scheduled nightly
- STAGING_RESULT is success only when all needed jobs succeed (fixes
cancelled runs skipping Slack)
- Post summary on every finished gated run (schedule or post_to_slack),
not just failures
Dependency: requires scripts/slack_summary.py e2e-staging mode in
synonymdev/bitkit-e2e-tests (parallel PR).
Co-authored-by: piotr-iohk <piotr-iohk@users.noreply.github.com>
Co-authored-by: piotr-iohk <piotr-iohk@users.noreply.github.com>
jvsena42
left a comment
There was a problem hiding this comment.
Re-reviewed at f26144786. The shared-script refactor is sound and the env-var fix is complete. Three LOW notes, none blocking — all three apply equally to the Android twin.
The env-var contract now matches exactly, which is what the second commit claims and it delivers. All nine names the script reads (SLACK_WEBHOOK_URL, PLATFORM, STAGING_RESULT, BUILD_RESULT, E2E_BRANCH_RESULT, E2E_TESTS_RESULT, RUN_URL, RUN_ATTEMPT, E2E_TESTS_REF) are supplied at lines 273-281, with no orphans in either direction; cross-checked against the script's own fixtures in scripts/test_slack_summary.py. BUILD_STAGING_RESULT -> BUILD_RESULT and E2E_TESTS_STAGING_RESULT -> E2E_TESTS_RESULT were the two that were wrong, and both are fixed. Worth noting the failure mode this avoided: every read uses os.environ.get(..., default), so a mismatch yields "unknown" in the message rather than a failed step — it would have shipped quietly.
Notification paths enumerated at head, since the point of the change was to stop dropping them:
| Path | Fires? | STAGING_RESULT |
|
|---|---|---|---|
| All shards pass | yes | success |
deliberate — input renamed to post_to_slack, this is now a summary not just an alert |
| A shard fails | yes | failure |
|
e2e-tests-staging times out at 90 min |
yes | failure |
GitHub reports a timed-out job as failure |
| Run cancelled | yes — always() survives cancellation |
failure |
gap closed; per-job fields still render cancelled, so the message is self-diagnosing |
build-staging fails, tests skipped |
yes | failure |
greptile's P1, genuinely closed |
e2e-branch fails |
yes | failure |
outputs.branch is empty, so the checkout falls back to the default branch and the script is still there |
| PR / push | no | — | correct, no such triggers |
Dispatch with post_to_slack: false |
no | — | correct |
I verified always() really does survive a concurrency cancellation rather than assuming it: the structurally identical e2e-status job in e2e.yml ran and reported failure in cancelled runs 34485911442 and 34482039396 while its upstreams were cancelled.
Script safety is strictly better than what it replaced. It is Python invoked as python3 scripts/…, so the exec bit and set -euo pipefail are moot (the blob is 100755 anyway). Zero ${{ }} reaches a shell command — every value goes through env: and line 282 is a literal. The webhook is an env: var, never on the command line, so it cannot leak via ps or a set -x trace. And one fewer third-party action to pin on each side.
Concurrency (line 27): the guard does not do what the commit message says, and this one is my fault — I suggested that expression on the Android side last round. cancel-in-progress is evaluated for the incoming run and decides whether that run cancels what is already in the group; it cannot inspect the run in flight. GitHub's documented idiom only protects main because github.ref is also in the group key, so both runs in a group necessarily evaluate it identically. Here github.event_name is not in the group key, so a dispatch on master shares the nightly's group, evaluates 'workflow_dispatch' != 'schedule' -> true, and cancels it.
Simplest correct shape, and it matches what every other workflow in both repos already does:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: trueIts correctness doesn't depend on the evaluation-context detail at all, which is the main reason to prefer it. There is also a second-order case the expression makes worse: in the reverse order, a schedule run arriving while a dispatch is in flight evaluates false, so it goes pending rather than cancelling — and a pending run is evicted by the next queued run in the group, so two dispatches in the overnight window can drop the nightly before it ever starts.
I am calling this LOW rather than blocking because the consequence I originally cared about is already fixed: a cancelled nightly now posts failure with cancelled in the per-job fields, so the signal isn't lost, just noisy. Reaching it also needs a dispatch inside the 04:00 UTC window. Note e2e_migration.yml in both repos has the same ref-only group with both triggers, so this predates the PR — and the mechanism is demonstrably live: on 2026-09-10, e2e_migration.yml run 34531572432 was cancelled 21 seconds after 34531814945 was created, both workflow_dispatch on refs/heads/master.
Re-verified clean, all at unchanged line numbers: SWIFT_ACTIVE_COMPILATION_CONDITIONS='DEBUG E2E_BUILD' (99); E2E_BACKEND: network / E2E_NETWORK: regtest (81-82), still resolving to ssl://electrs.bitkit.stag0.blocktank.to:9999; continue-on-error on attempts 1-2 only (204, 215) with attempt 3 bare; .outcome not .conclusion (216, 227, 239); timeout-minutes: 90 (122); cron 0 4 * * * not colliding with e2e_migration.yml's 0 3 * * *; artifact names consistent between upload (111) and download (154). No other workflow was switched to the script — the diff is one file, and scanning every workflow at head for slackapi|slack_summary|SLACK_WEBHOOK matches only this one.
|
Suggest we merge with the remaining Lows and enhance later. This is a new schedule + workflow_dispatch flow — hard to fully exercise Slack/nightly behaviour until it lands on master. The Low notes (Slack script checkout on feature e2e branches; fail-open webhook) are valid follow-ups, but they should not block landing PR A. Happy to follow up in a small PR after we have a real scheduled/dispatch run to look at. |
Include event_name in the concurrency group so a workflow_dispatch on master cannot cancel a scheduled nightly (or vice versa). Co-authored-by: piotr-iohk <piotr-iohk@users.noreply.github.com>
There was a problem hiding this comment.
Reaudit f261447..97e450c, 1 file. e2e-staging.yml: lines 26-27 now group runs by event name, so a dispatch cannot cancel the nightly. No findings.
Security
Skipped: no security surface in the diff (score 0, threshold 3).
Coverage
Total: 0% (delta diff since f261447, 1 file)
- Journeys: 0% - The delta changes a CI workflow concurrency key; it implements, changes and exercises no user journey.
- Unit tests: 0% - No suite in this repository covers
e2e-staging.yml, and the delta adds no test. - QA: 0% - Manual Tests not run
Reviewed by Claude Code (claude-opus-5-xhigh) via gh-pr-review-loop skill
addressed - reaudit confirmed
Summary
Adds a new E2E Staging workflow (
.github/workflows/e2e-staging.yml) for nightly regression testing and manual dispatch against staging/regtest infrastructure.Triggers: nightly schedule (cron
0 4 * * *) andworkflow_dispatchwith optional inputs. No PR trigger — this workflow runs only on schedule or manual dispatch.Changes
New workflow:
e2e-staging.ymlwith:0 4 * * *) and manual dispatch triggers onlyE2E_BUILDflag andregtestnetwork configsynonymdev/bitkit-e2e-testsrepo@multi_address_2,@pubky,@transfer_maxscripts/slack_summary.pyscriptInputs:
e2e_branch— E2E tests branch to use (default:default-feature-branch)post_to_slack— checkbox to post E2E staging summary to Slack (default: off)Slack notification gating:
#bitkit-staging-nightlypost_to_slackcheckbox is checkedSTAGING_RESULTis success only when all needed jobs succeed (catches cancelled runs)Concurrency:
${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}withcancel-in-progress: true, so schedule and dispatch never share a group and cannot cancel each otherDependencies
scripts/slack_summary.pywithe2e-stagingmode (merged to main)Test Plan
Notes
@transfer_1temporarily omitted from staging shard pending e2e-tests fix;@transfer_maxkeptdefault-feature-branchlogic fromdetermine-e2e-branch.ymltimeout-minutes: 90retained for e2e-tests-staging jobDesign
N/A — no UI changes.