Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
f342d0f
Merge pull request #708 from mindbox-cloud/master
AndreyEmtsov May 22, 2026
da1160e
MOBILE-0000: Treat trunk 409 Conflict as success in pod push wrapper …
justSmK May 25, 2026
0916fe1
MOBILE-182: Pin DateFormatter locale to fix dateTimeUtc on InAppShowF…
Vailence May 27, 2026
a982ef6
Update dependency fastlane to v2.235.0 (#710)
renovate[bot] May 31, 2026
1704251
Lock file maintenance (#711)
renovate[bot] Jun 1, 2026
b5b0788
MOBILE-183: Persist dates as fixed-pattern UTC and stabilize install/…
Vailence Jun 9, 2026
566363c
MOBILE-0000: Fix flaky InappScheduleManager delay-0 tests (#713)
justSmK Jun 9, 2026
2182864
MOBILE-121: Remove unused LOOP notify jobs & fix actionlint finding
sergeysozinov Jun 15, 2026
336ab07
MOBILE-0000: Publish unit-test reports on CI runs (#717)
justSmK Jun 15, 2026
042fcc3
MOBILE-0000: Tests Coverage (#718)
justSmK Jun 15, 2026
f3c25d3
MOBILE-208: Stop blocking the UI thread in public SDK methods
justSmK Jun 11, 2026
9929cc4
MOBILE-208: Behavioral tests for the operations pipeline contract
justSmK Jun 11, 2026
e84a263
MOBILE-0000: Silence unused-variable warnings in TrackVisitManagerTests
justSmK Jun 11, 2026
e7c78df
MOBILE-208: Serialize the public-operations contract suite
justSmK Jun 15, 2026
b8f20c7
Merge pull request #716 from mindbox-cloud/feature/MOBILE-208-ui-thre…
justSmK Jun 16, 2026
71b7b0c
MOBILE-0000: Run all unit-test targets via a shared test plan
justSmK Jun 15, 2026
4825769
MOBILE-0000: Move Logger unit tests into the MindboxLoggerTests target
justSmK Jun 15, 2026
be9518f
MOBILE-0000: Add sanitizer plans under TestPlans/, scope coverage
justSmK Jun 15, 2026
2b6946a
MOBILE-0000: Stop hardcoding the development team
justSmK Jun 15, 2026
7dd50fc
MOBILE-0000: Normalize target bundle IDs and drop Logger's Mac Catalyst
justSmK Jun 15, 2026
b3a1f22
MOBILE-0000: Add nightly sanitizer fastlane lanes (not wired to CI)
justSmK Jun 15, 2026
40b542a
MOBILE-0000: Raise MindboxNotificationsTests to iOS 13
justSmK Jun 15, 2026
27ccea8
Merge pull request #719 from mindbox-cloud/feature/test-plan
justSmK Jun 16, 2026
4fe6223
MOBILE-0000: Raise MindboxNotifications test coverage to ~96%
justSmK Jun 16, 2026
3145bd0
MOBILE-0000: Drop unused MindboxLogger dependency from MindboxNotific…
justSmK Jun 16, 2026
5e16084
Merge pull request #720 from mindbox-cloud/feature/mindbox-notificati…
justSmK Jun 16, 2026
d3db6cd
MOBILE-121: Add Gitleaks secret-scanning workflow (#721)
justSmK Jun 17, 2026
c997c71
Delete .github/pull_request_template.md (#722)
justSmK Jun 17, 2026
bff245f
MOBILE-206: Fix infinite layout loop in modal in-app messages (#723)
Vailence Jun 22, 2026
73e7624
MOBILE-0000: Update bundler deps; fix concurrent-ruby & faraday CVEs …
justSmK Jun 25, 2026
3d8564c
MOBILE-0000: Simplify fastlane — Ruby Fastfile, drop swift runner & a…
justSmK Jun 26, 2026
890d4dd
MOBILE-187: Remove AppGroup misconfiguration crash (#725)
justSmK Jun 26, 2026
7d8a185
MOBILE-187: Make MindboxLoggerTests a synchronized group
justSmK Jun 22, 2026
6c9aa87
MOBILE-187: Add MindboxLogger unit tests (coverage 54%->91%)
justSmK Jun 22, 2026
edf1c61
MOBILE-187: Migrate logger XCTest suites to Swift Testing
justSmK Jun 22, 2026
e9f7569
MOBILE-187: Tighten logger test assertions and isolation
justSmK Jun 26, 2026
eec0007
MOBILE-187: Convert MindboxNotificationsTests and TestPlans to folders
justSmK Jun 26, 2026
ddb7c59
Merge pull request #729 from mindbox-cloud/MOBILE-187-logger-coverage
justSmK Jun 26, 2026
d007dc9
MOBILE-230: Fix sync operation error delivery and date-migration hour…
Vailence Jun 29, 2026
481c046
MOBILE-213: Send in-app tags in tracking events and WebView operation…
Vailence Jul 8, 2026
cc309d8
MOBILE-268: Harden the WebView in-app show
justSmK Jul 8, 2026
7cfdaaf
MOBILE-268: Test the ready checker and timeout-error category
justSmK Jul 8, 2026
fe1c1ba
MOBILE-268: Add the shared WebView cache and feature toggles
justSmK Jul 8, 2026
807a127
MOBILE-268: Test the WebView cache store, fetcher, factory, and toggles
justSmK Jul 8, 2026
229799e
MOBILE-268: Backfill tests for the DI lock and UA builder
justSmK Jul 8, 2026
062dddf
MOBILE-268: Ignore a prior owner's callbacks on a reused bridge
justSmK Jul 8, 2026
2818ba4
MOBILE-268: Test the reused-bridge staleness filter and message gate
justSmK Jul 8, 2026
517ce67
MOBILE-268: Test the reused-bridge non-provisional didFail filter
justSmK Jul 8, 2026
c6b4022
MOBILE-268: Prewarm the in-app WebView and reuse it for the show
justSmK Jul 8, 2026
7183995
MOBILE-268: Test the prewarm service, planner, and learned-hosts store
justSmK Jul 8, 2026
b01f6c5
MOBILE-268: Test host persistence and the content-fetch fallback
justSmK Jul 8, 2026
d43358d
MOBILE-268: Drop two redundant comments in the new WebView code
justSmK Jul 9, 2026
af0e3c6
MOBILE-268: Remove restate-the-code comments in the WebView facade
justSmK Jul 9, 2026
1ca8ee8
MOBILE-268: Pass new toggle args in FeatureToggles test call sites
justSmK Jul 9, 2026
bbddad4
chore: regenerate Gemfile.lock on Bundler 4 (fastlane 2.237.0)
justSmK Jul 9, 2026
8403c1f
chore: tighten per-test timeout allowance to 60s
justSmK Jul 9, 2026
3ebf206
MOBILE-197: Send data-only payload in WebView sync onError
justSmK Jul 15, 2026
ed9b1f1
MOBILE-197: Cover data-only sync error payload for all MindboxError k…
justSmK Jul 15, 2026
c9e5b9e
MOBILE-197: Make the error-JSON fallback valid JSON
justSmK Jul 15, 2026
49811c6
MOBILE-197: Cover invalidResponse and unknown data-only payloads
justSmK Jul 15, 2026
840275a
Bump SDK version from 2.15.1 to 2.15.2
Jul 20, 2026
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
24 changes: 14 additions & 10 deletions .github/git-release.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/bin/bash
set -e
#!/usr/bin/env bash
set -euo pipefail

podspec_file="Mindbox.podspec"
version=$(grep -E '^\s+spec.version\s+=' "$podspec_file" | cut -d'"' -f2)
Expand All @@ -9,7 +9,8 @@ if [[ $version == *"rc"* ]]; then
is_beta=true
fi

branch=${GITHUB_REF#refs/heads/}
branch=${RELEASE_BRANCH:-${GITHUB_REF:-}}
branch=${branch#refs/heads/}

echo "Version: $version"
echo "Is Beta: $is_beta"
Expand All @@ -18,29 +19,32 @@ echo "Branch: $branch"
echo "Fetching tags from remote repository."
git fetch --tags

if git rev-parse "$version" >/dev/null 2>&1; then
if git rev-parse -q --verify "refs/tags/$version" >/dev/null; then
echo "Local tag $version already exists."
else
echo "Creating local tag $version."
git tag $version
git tag "$version"
fi

if git ls-remote --tags origin | grep -q "refs/tags/$version"; then
if [ -n "$(git ls-remote --tags origin "refs/tags/$version")" ]; then
echo "Remote tag $version already exists."
else
echo "Pushing local tag $version to remote."
git push origin $version
git push origin "$version"
fi

# Create release
release_title="Release $version"
text="Autogenerated release. Check more details at https://developers.mindbox.ru/docs/ios-sdk-integration"
text="Autogenerated release. Check more details at https://developers.mindbox.ru/docs/ios-sdk"

echo "Release title: $release_title"
echo "Text: $text"

prerelease_flag=$([ "$is_beta" = true ] && echo "--prerelease" || echo "")
release_args=("$version" --target "$branch" --title "$release_title" --notes "$text")
if [ "$is_beta" = true ]; then
release_args+=(--prerelease)
fi

gh release create "$version" --target "$branch" --title "$release_title" --notes "$text" $prerelease_flag
gh release create "${release_args[@]}"

echo "Release $version created for repo on branch $branch"
35 changes: 35 additions & 0 deletions .github/pod-trunk-push.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# Wrapper around `pod trunk push` that treats a 409 Conflict
# ("Unable to accept duplicate entry") from trunk as success.
#
# Why: trunk sometimes returns 504 Gateway Timeout while the spec
# still lands in CocoaPods/Specs (Heroku 30s router cap on the
# GitHub Commits API call). Without this wrapper, re-running the
# failed publish job fails again with 409 "duplicate entry",
# keeping the workflow red even though the release is actually done.

set -uo pipefail

if [ $# -lt 1 ]; then
echo "usage: $0 <Pod.podspec> [pod trunk push args...]" >&2
exit 64
fi

PODSPEC="$1"; shift

set +e
OUT="$(pod trunk push "$PODSPEC" "$@" 2>&1)"
RC=$?
set -e
printf '%s\n' "$OUT"

if [ "$RC" -eq 0 ]; then
exit 0
fi

if grep -qE 'HTTP/1\.[01] 409 Conflict|Unable to accept duplicate entry' <<<"$OUT"; then
echo "[pod-trunk-push] trunk returned 409 — version already published, treating as success"
exit 0
fi

exit "$RC"
29 changes: 0 additions & 29 deletions .github/pull_request_template.md

This file was deleted.

229 changes: 229 additions & 0 deletions .github/scripts/generate_coverage_site.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
#!/usr/bin/env python3
"""Builds a static coverage site (index.html + history.json + coverage.csv) from an .xcresult.

Usage: generate_coverage_site.py <path.xcresult> <output_dir> [previous_history.json]

Coverage comes from `xcrun xccov view --report --json`; *.xctest bundles are
excluded so the numbers reflect the shipped frameworks only. History is carried
forward by the caller: the workflow downloads the currently published
history.json from the live Pages site and passes it in; this script appends
today's point and ships the merged file with the new site, so the trend
survives even though every Pages deploy replaces the whole site.

The page is fully self-contained: inline CSS/JS, no external resources.
"""
import csv
import html
import json
import os
import subprocess
import sys
from datetime import datetime, timezone

HISTORY_LIMIT = 1000


def pct(value):
return f"{value * 100:.1f}%"


def bar(value):
width = round(value * 100, 1)
color = "#2da44e" if value >= 0.7 else "#d4a72c" if value >= 0.4 else "#cf222e"
return (f'<div class="track"><div style="background:{color};width:{width}%;'
f'height:10px;border-radius:3px"></div></div>')


def load_history(path):
if not path or not os.path.exists(path):
return []
try:
history = json.load(open(path))
if not isinstance(history, list):
return []
except (json.JSONDecodeError, OSError):
return []
# Points written before the ts field existed carry only the human-readable
# UTC string; synthesize ts from it so all dates render uniformly.
for point in history:
if "ts" not in point:
try:
parsed = datetime.strptime(point.get("date", ""), "%Y-%m-%d %H:%M UTC")
point["ts"] = parsed.replace(tzinfo=timezone.utc).isoformat(timespec="seconds")
except ValueError:
pass
return history


def repo_relative(path, repo):
workspace = os.environ.get("GITHUB_WORKSPACE")
if workspace and path.startswith(workspace + "/"):
return path[len(workspace) + 1:]
marker = f"/{repo.rsplit('/', 1)[-1]}/"
idx = path.rfind(marker)
return path[idx + len(marker):] if idx >= 0 else None


def trend_svg(history):
points = [p["coverage"] for p in history][-90:]
if len(points) < 2:
return "<p>Trend appears after the second deploy.</p>"
lo, hi = min(points), max(points)
pad = max((hi - lo) * 0.15, 0.5)
lo, hi = lo - pad, hi + pad
w, h = 640, 120
step = w / (len(points) - 1)
coords = " ".join(f"{i * step:.1f},{h - (v - lo) / (hi - lo) * h:.1f}" for i, v in enumerate(points))
return (f'<svg width="{w}" height="{h}" class="chart">'
f'<polyline fill="none" stroke="#2da44e" stroke-width="2" points="{coords}"/></svg>'
f'<p class="muted">last {len(points)} deploys, {points[0]:.1f}% &rarr; {points[-1]:.1f}%</p>')


def write_csv(path, targets, repo):
with open(path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["target", "file", "path", "executable_lines",
"covered_lines", "uncovered_lines", "line_coverage_percent"])
for t in targets:
for fl in t.get("files", []):
writer.writerow([
t["name"], fl["name"],
repo_relative(fl.get("path", ""), repo) or fl.get("path", ""),
fl["executableLines"], fl["coveredLines"],
fl["executableLines"] - fl["coveredLines"],
round(fl["lineCoverage"] * 100, 2),
])


def main():
xcresult, outdir = sys.argv[1], sys.argv[2]
previous_history = sys.argv[3] if len(sys.argv) > 3 else None
repo = os.environ.get("GITHUB_REPOSITORY", "mindbox-cloud/ios-sdk")
branch = os.environ.get("GITHUB_REF_NAME", "local")
sha = os.environ.get("GITHUB_SHA", "local")[:9]
now = datetime.now(timezone.utc)
date = now.strftime("%Y-%m-%d %H:%M UTC")
iso = now.isoformat(timespec="seconds")

report = json.loads(subprocess.run(
["xcrun", "xccov", "view", "--report", "--json", xcresult],
check=True, capture_output=True).stdout)
targets = [t for t in report["targets"] if not t["name"].endswith(".xctest")]
executable = sum(t["executableLines"] for t in targets)
covered = sum(t["coveredLines"] for t in targets)
overall = covered / executable if executable else 0.0

history = load_history(previous_history)
if not history or history[-1].get("sha") != sha:
history.append({
"date": date,
"ts": iso,
"branch": branch,
"sha": sha,
"coverage": round(overall * 100, 2),
"targets": {t["name"]: round(t["lineCoverage"] * 100, 2) for t in targets},
})
history = history[-HISTORY_LIMIT:]

target_rows = "".join(
f"<tr><td>{html.escape(t['name'])}</td><td>{pct(t['lineCoverage'])}</td>"
f"<td>{bar(t['lineCoverage'])}</td><td>{t['coveredLines']}/{t['executableLines']}</td></tr>"
for t in targets)

file_sections = []
for t in targets:
rows = []
# Impact order: most uncovered lines first (size x (1 - coverage)),
# percentage as the tie-breaker.
for f in sorted(t.get("files", []),
key=lambda f: (f["coveredLines"] - f["executableLines"], f["lineCoverage"])):
name = html.escape(f["name"])
rel = repo_relative(f.get("path", ""), repo)
cell = (f'<a href="https://github.com/{repo}/blob/{sha}/{html.escape(rel)}">{name}</a>'
if rel else name)
rows.append(
f'<tr data-file="{name.lower()}"><td title="{html.escape(f.get("path", ""))}">{cell}</td>'
f"<td><b>{f['executableLines'] - f['coveredLines']}</b></td>"
f"<td>{pct(f['lineCoverage'])}</td><td>{bar(f['lineCoverage'])}</td>"
f"<td>{f['coveredLines']}/{f['executableLines']}</td></tr>")
file_sections.append(
f"<details><summary><b>{html.escape(t['name'])}</b> — {pct(t['lineCoverage'])}, "
f"{len(t.get('files', []))} files (most uncovered lines first)</summary>"
f"<table><tr><th>File</th><th>Uncovered</th><th>Coverage</th><th></th><th>Lines</th></tr>{''.join(rows)}</table></details>")

history_rows = "".join(
f"<tr><td><span class=\"ts\" data-ts=\"{html.escape(p.get('ts', ''))}\">{html.escape(p['date'])}</span></td>"
f"<td><a href=\"https://github.com/{repo}/commit/{html.escape(p['sha'])}\">{html.escape(p['sha'])}</a></td>"
f"<td>{p['coverage']:.2f}%</td></tr>"
for p in reversed(history[-30:]))

page = f"""<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8"><title>Mindbox iOS SDK — Code Coverage</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root {{ --bg: #ffffff; --fg: #1f2328; --muted: #666; --line: #eee; --track: #eee; --chart-bg: #fafafa }}
@media (prefers-color-scheme: dark) {{
:root {{ --bg: #0d1117; --fg: #e6edf3; --muted: #8b949e; --line: #30363d; --track: #21262d; --chart-bg: #161b22 }}
}}
body {{ font: 14px -apple-system, sans-serif; margin: 2em auto; max-width: 880px; color: var(--fg); background: var(--bg) }}
a {{ color: #4493f8 }}
table {{ border-collapse: collapse; margin: 0.7em 0 }}
td, th {{ padding: 4px 12px; border-bottom: 1px solid var(--line); text-align: left }}
details {{ margin: 0.6em 0 }}
summary {{ cursor: pointer }}
input {{ font: inherit; padding: 5px 9px; width: 260px; border: 1px solid var(--line); border-radius: 6px;
background: var(--bg); color: var(--fg) }}
button {{ font: inherit; padding: 5px 10px; border: 1px solid var(--line); border-radius: 6px;
background: var(--bg); color: var(--fg); cursor: pointer }}
.big {{ font-size: 42px; font-weight: 700 }}
.muted {{ color: var(--muted) }}
.track {{ background: var(--track); border-radius: 3px; width: 120px; display: inline-block }}
.chart {{ background: var(--chart-bg); border: 1px solid var(--line) }}
</style></head><body>
<h1>Mindbox iOS SDK — Code Coverage</h1>
<p class="big">{pct(overall)}</p>
<p class="muted">{branch} @ <a href="https://github.com/{repo}/commit/{sha}">{sha}</a> &middot; <span class="ts" data-ts="{iso}">{date}</span> &middot; test bundles excluded</p>
<p class="muted">Export: <a href="coverage.csv" download="coverage-{sha}.csv">per-file CSV</a> &middot; <a href="history.json" download="history-{sha}.json">trend JSON</a></p>
<h2>Targets</h2>
<table><tr><th>Target</th><th>Coverage</th><th></th><th>Lines</th></tr>{target_rows}</table>
<h2>Trend</h2>
{trend_svg(history)}
<h2>Files</h2>
<p>
<input id="q" type="search" placeholder="Filter files&hellip;" oninput="filterFiles(this.value)">
<button onclick="setAll(true)">Expand all</button>
<button onclick="setAll(false)">Collapse all</button>
</p>
{''.join(file_sections)}
<h2>Recent deploys</h2>
<table><tr><th>Date</th><th>Commit</th><th>Total</th></tr>{history_rows}</table>
<script>
function setAll(open) {{
document.querySelectorAll('details').forEach(d => d.open = open);
}}
document.querySelectorAll('.ts[data-ts]').forEach(el => {{
const d = new Date(el.dataset.ts);
if (!isNaN(d)) {{ el.title = el.textContent + ' (UTC)'; el.textContent = d.toLocaleString(); }}
}});
function filterFiles(query) {{
const q = query.trim().toLowerCase();
if (q) setAll(true);
document.querySelectorAll('tr[data-file]').forEach(row => {{
row.style.display = row.dataset.file.includes(q) ? '' : 'none';
}});
}}
</script>
</body></html>
"""
os.makedirs(outdir, exist_ok=True)
with open(os.path.join(outdir, "index.html"), "w") as f:
f.write(page)
with open(os.path.join(outdir, "history.json"), "w") as f:
json.dump(history, f, indent=1)
write_csv(os.path.join(outdir, "coverage.csv"), targets, repo)
print(f"coverage site: {pct(overall)} overall, {len(targets)} targets, "
f"{len(history)} history points -> {outdir}/")


if __name__ == "__main__":
main()
6 changes: 5 additions & 1 deletion .github/workflows/distribute-develop-mission.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ on:
types:
- closed

permissions:
contents: read

jobs:
call-reusable:
if: ${{ github.event.pull_request.merged == true }}
uses: ./.github/workflows/distribute-reusable.yml
with:
branch: ${{ github.base_ref }}
secrets: inherit
secrets:
GITLAB_TRIGGER_TOKEN: ${{ secrets.GITLAB_TRIGGER_TOKEN }}
6 changes: 5 additions & 1 deletion .github/workflows/distribute-manual.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ on:
required: false
default: ""

permissions:
contents: read

jobs:
call-reusable:
uses: ./.github/workflows/distribute-reusable.yml
with:
branch: ${{ github.ref_name }} # SDK branch = current branch
app_ref: ${{ inputs.app_ref }} # optional override for GitLab ref
secrets: inherit
secrets:
GITLAB_TRIGGER_TOKEN: ${{ secrets.GITLAB_TRIGGER_TOKEN }}
Loading