Skip to content
Merged
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
20 changes: 20 additions & 0 deletions .af-e2e/env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Credentials for the QA app id `com.appsflyer.qa.cordova` (create in AppsFlyer HQ).
# Same key=value lines are used locally and in CI.
#
# Local E2E sibling (default: ../<repo-basename>-e2e/.env)
# `./scripts/e2e-cordova-build.sh` runs `write-e2e-env-to-dir.sh` and writes sibling `.env` from:
# `ENV_FILE` (CI), else `.af-e2e/.env.local`, else `test-app/.env`.
# Or seed an empty template: ./scripts/bootstrap-e2e-env.sh then edit sibling `.env`.
#
# GitHub Actions
# Repository secret `ENV_FILE`: pass on the **Build Cordova E2E** step as `env: ENV_FILE: ${{ secrets.ENV_FILE }}`.
# `e2e-cordova-build.sh` calls `write-e2e-env-to-dir.sh`, which writes the sibling `.env` (same body
# as a pasted multiline secret). Typical lines:
# DEV_KEY=<your_dev_key>
# APP_ID=<numeric_apple_app_store_id>
# Use the **Apple ID** from App Store Connect (App Information), digits only. Until you have one,
# a throwaway placeholder like `2485629345` can unblock local init; replace before relying on HQ data.
# Lines starting with `#` in `.env` are ignored by `hooks/emit-af-qa-env.js`.
#
DEV_KEY=
APP_ID=
26 changes: 11 additions & 15 deletions .af-e2e/test-plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"_meta": {
"plan_id": "cordova-e2e",
"plugin": "cordova",
"version": "1.0.0",
"version": "1.0.3",
"description": "End-to-end test plan for the AppsFlyer Cordova plugin. build_cmd syncs test-app/ to a sibling directory (../appsflyer-cordova-plugin-e2e by default) then builds with file: plugin path. Covers E2E-001..E2E-006 per appsflyer-mobile-plugin-tooling/contracts/e2e-test-contract.md.",
"platforms": [
"android",
Expand Down Expand Up @@ -104,7 +104,7 @@
"id": "http_200_count",
"description": "At least 3 HTTP 200 responses from AppsFlyer servers",
"type": "count_matches",
"pattern": "response code:200 OK|response_status=200",
"pattern": "response code:\\s*200(\\s*OK)?|response_status=200",
"minimum": 3,
"fail_action": "fail"
},
Expand Down Expand Up @@ -134,18 +134,16 @@
"id": "phase_2",
"name": "Background deep link",
"scenario_ref": "E2E-002",
"description": "App is brought down after Phase 1 SDK start; the deep-link URL re-launches it. On Android the app is HOME-keyed to background and re-entered via VIEW intent. On iOS the app is terminated and re-launched with `-deepLinkURL <url>`, which the Cordova app's URL handler / AppDelegate hook (same deep link pipeline as production) \u2014 same SDK pipeline as a real custom-scheme open, but without iOS 17/18-simulator's 'Open in <App>?' confirmation prompt that `simctl openurl` triggers and that nothing in a non-interactive CI run can dismiss. Both paths fire onDeepLinking with Status.FOUND.",
"description": "App is brought down after Phase 1 SDK start; the deep-link URL re-launches it. On Android the app is sent to the background by starting the default launcher (`ACTION_MAIN` + `CATEGORY_HOME`) with **2s on-device dwell** in one `adb shell` compound — then re-entered via VIEW intent. On iOS the app is terminated with **`simctl terminate` then `sleep 2` on the same shell line** (runner `eval`) so the process fully tears down before `-deepLinkURL` relaunch; avoids a separate host-only `sleep` that can race the trigger. Same SDK pipeline as a real custom-scheme open without iOS 17/18-simulator's `simctl openurl` confirmation prompt. Both paths fire onDeepLinking with Status.FOUND.",
"requires_fresh_install": false,
"wait_after_trigger_sec": 90,
"deep_link_url": "afqa-cordova://deeplink?deep_link_value=qa_deeplink_bg&af_sub1=background_test&pid=testmedia&c=deeplink_test",
"pre_actions": {
"android": [
"adb shell input keyevent KEYCODE_HOME",
"sleep 2"
"adb shell \"am start -a android.intent.action.MAIN -c android.intent.category.HOME && sleep 2\""
],
"ios": [
"xcrun simctl terminate {{UDID}} {{BUNDLE_ID}}",
"sleep 1"
"xcrun simctl terminate {{UDID}} {{BUNDLE_ID}} && sleep 2"
]
},
"trigger": {
Expand Down Expand Up @@ -183,19 +181,17 @@
"id": "phase_3",
"name": "Foreground deep link",
"scenario_ref": "E2E-003",
"description": "Fresh install. App is in foreground after SDK start. On Android a brief HOME switch triggers onPause and the VIEW intent brings the app back. On iOS the app is terminated and re-launched with `-deepLinkURL <url>` (same launch-arg path as phase_2), since `simctl openurl` against a running iOS-simulator app still triggers the 'Open in <App>?' confirmation prompt; the foreground-vs-killed distinction is Android-only. Both paths fire onDeepLinking with Status.FOUND.",
"description": "Fresh install. App is in foreground after SDK start. On Android a brief switch to the default launcher (`ACTION_MAIN` + `CATEGORY_HOME`) triggers `onPause` without killing the process; the same **2 second on-device dwell** as E2E phase_2 (gesture-navigation friendly) runs before the VIEW deep-link intent brings the app back. On iOS **`simctl terminate` + `sleep 2`** on one shell line (same as phase_2), then `-deepLinkURL` relaunch; `simctl openurl` against a running simulator still triggers the 'Open in <App>?' prompt. The foreground-vs-killed distinction is Android-only. Both paths fire onDeepLinking with Status.FOUND.",
"requires_fresh_install": true,
"wait_after_launch_sec": 240,
"wait_after_trigger_sec": 90,
"deep_link_url": "afqa-cordova://deeplink?deep_link_value=qa_deeplink_fg&af_sub1=foreground_test&pid=testmedia&c=deeplink_test",
"pre_actions": {
"android": [
"adb shell am start -a android.intent.action.MAIN -c android.intent.category.HOME",
"sleep 1"
"adb shell \"am start -a android.intent.action.MAIN -c android.intent.category.HOME && sleep 2\""
],
"ios": [
"xcrun simctl terminate {{UDID}} {{BUNDLE_ID}}",
"sleep 1"
"xcrun simctl terminate {{UDID}} {{BUNDLE_ID}} && sleep 2"
]
},
"trigger": {
Expand Down Expand Up @@ -287,7 +283,7 @@
"id": "custom_event_http_200",
"description": "Custom event request returns HTTP 200",
"type": "count_matches",
"pattern": "response code:200 OK|response_status=200",
"pattern": "response code:\\s*200(\\s*OK)?|response_status=200",
"minimum": 1,
"fail_action": "fail"
},
Expand Down Expand Up @@ -361,7 +357,7 @@
"id": "identity_event_http_200",
"description": "Identity check event receives HTTP 200",
"type": "count_matches",
"pattern": "response code:200 OK|response_status=200",
"pattern": "response code:\\s*200(\\s*OK)?|response_status=200",
"minimum": 1,
"fail_action": "fail"
}
Expand Down Expand Up @@ -403,7 +399,7 @@
"id": "resumed_event_http_200",
"description": "After stop(false), af_qa_resumed produces HTTP 200",
"type": "count_matches",
"pattern": "response code:200 OK|response_status=200",
"pattern": "response code:\\s*200(\\s*OK)?|response_status=200",
"minimum": 1,
"fail_action": "fail"
},
Expand Down
16 changes: 6 additions & 10 deletions .af-smoke/rc-test-plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"_meta": {
"plan_id": "cordova-rc-smoke",
"plugin": "cordova",
"version": "1.0.0",
"version": "1.0.3",
"description": "Post-publish smoke plan for the AppsFlyer Cordova plugin. SMOKE-001..003 against test-app_rc_smoke/ (CI-synthesized; build_cmd runs scripts/smoke-cordova-build.sh which rsyncs test-app/ then pins cordova-plugin-appsflyer-sdk to the npm version from root package.json or SMOKE_PLUGIN_VERSION).",
"platforms": [
"android",
Expand Down Expand Up @@ -76,7 +76,7 @@
"id": "http_200_count",
"description": "At least 3 HTTP 200 responses from AppsFlyer servers",
"type": "count_matches",
"pattern": "response code:200 OK|response_status=200",
"pattern": "response code:\\s*200(\\s*OK)?|response_status=200",
"minimum": 3,
"fail_action": "fail"
},
Expand Down Expand Up @@ -105,12 +105,10 @@
"deep_link_url": "afqa-cordova://deeplink?deep_link_value=qa_deeplink_bg&af_sub1=background_test&pid=testmedia&c=deeplink_test",
"pre_actions": {
"android": [
"adb shell input keyevent KEYCODE_HOME",
"sleep 2"
"adb shell \"am start -a android.intent.action.MAIN -c android.intent.category.HOME && sleep 2\""
],
"ios": [
"xcrun simctl terminate {{UDID}} {{BUNDLE_ID}}",
"sleep 1"
"xcrun simctl terminate {{UDID}} {{BUNDLE_ID}} && sleep 2"
]
},
"trigger": {
Expand Down Expand Up @@ -155,12 +153,10 @@
"deep_link_url": "afqa-cordova://deeplink?deep_link_value=qa_deeplink_fg&af_sub1=foreground_test&pid=testmedia&c=deeplink_test",
"pre_actions": {
"android": [
"adb shell am start -a android.intent.action.MAIN -c android.intent.category.HOME",
"sleep 1"
"adb shell \"am start -a android.intent.action.MAIN -c android.intent.category.HOME && sleep 2\""
],
"ios": [
"xcrun simctl terminate {{UDID}} {{BUNDLE_ID}}",
"sleep 1"
"xcrun simctl terminate {{UDID}} {{BUNDLE_ID}} && sleep 2"
]
},
"trigger": {
Expand Down
19 changes: 19 additions & 0 deletions .claude/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# `.claude` — shared agent guidance (Claude Code + Cursor)

This directory holds **project skills** (`skills/*/SKILL.md`) so the same markdown can be used from:

- **Claude Code** — load skills from the project `.claude/` tree (per Claude Code project conventions).
- **Cursor** — the same skill folders are **symlinked** under `.cursor/skills/` so Cursor’s skill discovery picks them up. Do not duplicate `SKILL.md` bodies in two places; edit files under **`.claude/skills/`** only.

## Layout

| Path | Role |
|------|------|
| `.claude/skills/<name>/SKILL.md` | Canonical skill source |
| `.cursor/skills/<name>` | Symlink → `../../.claude/skills/<name>` |

If a symlink is missing after clone on Windows, enable `git config core.symlinks true` before checkout, or recreate the link from `.cursor/skills/` to `.claude/skills/`.

## Upstream templates

Skills were adapted from [`appsflyer-mobile-plugin-tooling/templates/`](https://github.com/AppsFlyerSDK/appsflyer-mobile-plugin-tooling/tree/main/templates) (`claude-skills/`, `cursor-skills/`). When contracts change, refresh content from a new tooling tag and update `scripts/TOOLING_PIN.txt` for the vendored runner.
51 changes: 51 additions & 0 deletions .claude/skills/cordova-rc-release/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
name: cordova-rc-release
description: >-
Maps the AppsFlyer Cordova RC pipeline (RC prep, lint-test-build, E2E, npm publish, rc-smoke,
promote) to workflow files in this repo. Use when editing rc-release, rc-smoke, promote-release,
or lint-test-build; not for day-to-day feature work.
globs: >-
.github/workflows/rc-release.yml,.github/workflows/rc-smoke.yml,.github/workflows/promote-release.yml,
.github/workflows/lint-test-build.yml,
.github/workflows/android-e2e.yml,.github/workflows/ios-e2e.yml,
.github/workflows/pre-release-workflow.yml,.github/workflows/release-Production-workflow.yml,
.af-e2e/**,.af-smoke/rc-test-plan.json,scripts/af-scenario-runner.sh
---

# RC release pipeline — AppsFlyer Cordova plugin

Use when:

- Editing **`rc-release.yml`**, **`rc-smoke.yml`**, **`promote-release.yml`**, or **`lint-test-build.yml`**.
- Wiring **`workflow_call`** from RC into **lint-test-build** + **Android E2E** + **iOS E2E**.
- Explaining why promotion should wait on **`rc-smoke/npm`** check-run **success** on the release branch head.

## Contract sources (shared tooling repo)

- [`contracts/rc-release-contract.md`](https://github.com/AppsFlyerSDK/appsflyer-mobile-plugin-tooling/blob/main/contracts/rc-release-contract.md)
- [`contracts/e2e-test-contract.md`](https://github.com/AppsFlyerSDK/appsflyer-mobile-plugin-tooling/blob/main/contracts/e2e-test-contract.md)
- [`contracts/smoke-test-contract.md`](https://github.com/AppsFlyerSDK/appsflyer-mobile-plugin-tooling/blob/main/contracts/smoke-test-contract.md)

## Stages in this repo (Cordova)

| Stage | Workflow(s) |
|-------|----------------|
| RC pre-publish | **`rc-release.yml`**: **`lint-test-build.yml`** (`workflow_call` only) + **`ios-e2e.yml`** + **`android-e2e.yml`**, then npm **`QA`** publish, prerelease, PR to `master` |
| Post-publish smoke | **`rc-smoke.yml`** → check **`rc-smoke/npm`** |
| Promote | **`promote-release.yml`** (label + **`rc-smoke/npm`** gate) → **`rc-promote-strip-rc.sh`** |
| Production (legacy) | **`release-Production-workflow.yml`** on merge to `master` — verify before changing |

Track adoption details in **`docs/SCENARIO_RUNNER_ADOPTION_WORKPLAN.md`**.

## Secrets (names only)

| Secret | Typical use |
|--------|-------------|
| `ENV_FILE` | Multiline `.env` for E2E sibling + smoke (`DEV_KEY`, `APP_ID`) |
| `CI_NPM_TOKEN` | `npm publish` in RC Release |
| `CI_DEV_GITHUB_TOKEN` | Branch push, PR, prerelease, promote, `gh` in smoke |

## Do not

- Do not bypass **`rc-smoke/npm` success** for promotion when using the designed promote gate.
- Do not duplicate long contract text in skills — link tooling contracts above.
87 changes: 87 additions & 0 deletions .claude/skills/cordova-scenario-runner/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
name: cordova-scenario-runner
description: >-
Run or triage AppsFlyer Cordova E2E/smoke via ./scripts/af-scenario-runner.sh, sibling E2E copy,
smoke dir, and JSON reports. Use for local reproduction, CI log triage, or report summaries.
---

# Scenario runner — AppsFlyer Cordova plugin

Use when:

- Android/iOS E2E or smoke failed in CI and you need to reproduce or summarize reports.
- Changing `.af-e2e/test-plan.json`, `.af-smoke/rc-test-plan.json`, or build scripts used by the runner.
- Validating install/launch paths after editing `test-app/` or sync scripts.

## Cordova-specific layout

| Piece | Location |
|-------|----------|
| Runner (vendored) | `scripts/af-scenario-runner.sh` (pin in `scripts/TOOLING_PIN.txt`) |
| E2E plan | `.af-e2e/test-plan.json` — build uses **sibling** `../<repo-basename>-e2e/` (default `../appsflyer-cordova-plugin-e2e/`) via `scripts/e2e-cordova-build.sh` |
| Smoke plan | `.af-smoke/rc-test-plan.json` — paths under `test-app_rc_smoke/` via `scripts/smoke-cordova-build.sh` |
| Reference app (do not `cordova build` here with `file:..`) | `test-app/` |
| Android CI scenario entry | `scripts/ci-android-e2e-scenario.sh` (single `bash …/ci-android-e2e-scenario.sh` line inside `android-emulator-runner`; see below) |
| Reports | `.af-e2e/reports/`, `.af-smoke/reports/` |

## Preconditions

- **Android:** booted emulator; `adb devices` shows `device`.
- **iOS:** booted simulator (`xcrun simctl list devices booted`).
- **`.env`:** for E2E, on the **sibling** copy (e.g. `../appsflyer-cordova-plugin-e2e/.env`) — CI writes from `ENV_FILE`; `sync-test-app-e2e-copy.sh` **protects** `.env` on rsync.
- **`jq`** installed.

## Commands (repo root)

Dry-run parse:

```sh
./scripts/af-scenario-runner.sh --platform android --plan .af-e2e/test-plan.json --dry-run
./scripts/af-scenario-runner.sh --platform ios --plan .af-e2e/test-plan.json --dry-run
```

Build + single phase (paths / install smoke):

```sh
./scripts/af-scenario-runner.sh --platform android --plan .af-e2e/test-plan.json --build --phase phase_1
./scripts/af-scenario-runner.sh --platform ios --plan .af-e2e/test-plan.json --build --phase phase_1
```

Smoke (after `scripts/sync-test-app-rc-smoke.sh` + local build):

```sh
./scripts/af-scenario-runner.sh --platform android --plan .af-smoke/rc-test-plan.json --build --phase phase_1
```

## Read the JSON report

```sh
jq '.overall_status, .total_checks, .passed, .failed' .af-e2e/reports/latest.json
```

Failed checks:

```sh
jq -r '
.phases[]
| .phase_id as $pid
| .checks | to_entries[]
| select(.value.status != "PASS")
| "\($pid)/\(.key): \(.value.evidence)"
' .af-e2e/reports/latest.json
```

## CI: Android `script:` must be one line

`reactivecircus/android-emulator-runner` runs **each line** of `with: script:` as a **separate** `/usr/bin/sh -c` invocation (on Ubuntu **`sh` is dash**). Multi-line `if`/`fi` in YAML **breaks**; **`set -o pipefail` is invalid in dash**. **`android-e2e.yml`** uses one line: **`bash "${GITHUB_WORKSPACE}/scripts/ci-android-e2e-scenario.sh"`** so bash-only options live in that script.

## Rules

- Do not edit generated `latest.json` / phase JSON as “fixes”; fix the app, plan, or environment.
- Exit non-zero from the runner means FAIL — do not report PASS.
- No secrets or raw `DEV_KEY` in summaries.
- Contract docs: `appsflyer-mobile-plugin-tooling` — `contracts/e2e-test-contract.md`, `contracts/smoke-test-contract.md`, `docs/troubleshooting.md`.

## Repo workplan

See `docs/SCENARIO_RUNNER_ADOPTION_WORKPLAN.md` for phases (E2E CI, test-app contract, RC smoke).
59 changes: 59 additions & 0 deletions .claude/skills/cordova-smoke-ci-alignment/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
name: cordova-smoke-ci-alignment
description: >-
Maps AppsFlyer Cordova CI (android/ios E2E workflows, ENV_FILE, sibling copy, smoke scripts) to
the tooling smoke/E2E story. Use when editing .github/workflows/*e2e*, rc-smoke (when added),
or debugging scenario CI.
globs: >-
.github/workflows/android-e2e.yml,.github/workflows/ios-e2e.yml,.github/workflows/lint-test-build.yml,
scripts/ci-android-e2e-scenario.sh,
scripts/af-scenario-runner.sh,scripts/e2e-cordova-build.sh,scripts/smoke-cordova-build.sh,
scripts/sync-test-app-e2e-copy.sh,scripts/sync-test-app-rc-smoke.sh,.af-e2e/**,.af-smoke/**
---

# Smoke / E2E CI alignment — AppsFlyer Cordova plugin

Use when:

- Editing or debugging **`android-e2e.yml`** / **`ios-e2e.yml`**.
- Explaining **`ENV_FILE`**, sibling **`.env`**, or artifact paths.
- Preparing or debugging **`rc-smoke.yml`** / **`promote-release.yml`**.

## What exists today

| Workflow | Role |
|----------|------|
| `.github/workflows/android-e2e.yml` | Ubuntu: Java 17, Android SDK, `e2e-cordova-build.sh android`, then emulator + **`bash …/ci-android-e2e-scenario.sh`** (single `script:` line) |
| `.github/workflows/ios-e2e.yml` | macOS: CocoaPods, `e2e-cordova-build.sh ios`, boot iPhone sim, `af-scenario-runner.sh` |

Triggers: `workflow_dispatch`, `workflow_call` (e.g. from **`rc-release.yml`**). Secrets: **`ENV_FILE`** (optional but required for real SDK checks).

**Note:** **`lint-test-build.yml`** is **`workflow_call` only** (invoked from **`rc-release.yml`** today).

## E2E vs smoke directories

| Flow | App directory | Plan | Build command in plan |
|------|---------------|------|------------------------|
| E2E | Sibling `../appsflyer-cordova-plugin-e2e/` (rsync + `file:` plugin URL) | `.af-e2e/test-plan.json` | `./scripts/e2e-cordova-build.sh <android\|ios>` |
| RC smoke | `test-app_rc_smoke/` (gitignored; `sync-test-app-rc-smoke.sh`) | `.af-smoke/rc-test-plan.json` | `./scripts/smoke-cordova-build.sh <android\|ios>` |

## Android emulator runner quirks

- **`script:` must be a single physical line** — use **`bash "${GITHUB_WORKSPACE}/scripts/ci-android-e2e-scenario.sh"`** (dash `sh` cannot run `set -o pipefail`; logic lives in bash).
- **KVM:** udev + `chmod 666 /dev/kvm` so the job does not fall back to **`-accel off`** (multi‑minute boots).
- **`emulator-options`:** include `-gpu swiftshader_indirect` and `-no-boot-anim` if you override defaults (full override replaces action defaults).

## Artifacts

Upload **`.af-e2e/reports/`** on both workflows (`if: always()`, `if-no-files-found: warn`). When smoke CI exists, mirror **`.af-smoke/reports/`**.

## Do not

- Do not bypass **`af-scenario-runner.sh`** for release assertions; JSON reports are the contract output.
- Do not commit **`.env`** or embed dev keys in YAML.
- Do not add `SMOKE-*` / `E2E-*` phases to JSON without updating the tooling contracts and test app.

## Cross-reference

- `docs/SCENARIO_RUNNER_ADOPTION_WORKPLAN.md`
- `appsflyer-mobile-plugin-tooling/docs/ci-alignment-guide.md`
Loading
Loading