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
12 changes: 9 additions & 3 deletions .github/workflows/e2e-manual.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,14 @@ jobs:
echo "simulator=$SIMULATOR_NAME" >> "$GITHUB_OUTPUT"

- name: Run iOS E2E
env:
E2E_RETRY_COUNT: "3"
E2E_RETRY_DELAY_SEC: "30"
run: |
if [ "${{ inputs.expo }}" = "true" ]; then
npm run e2e -- --app "${{ inputs.app }}" --platform ios --simulator "${{ steps.boot-simulator.outputs.simulator }}" --framework expo
npm run e2e -- --app "${{ inputs.app }}" --platform ios --simulator "${{ steps.boot-simulator.outputs.simulator }}" --framework expo --retry-count "$E2E_RETRY_COUNT" --retry-delay-sec "$E2E_RETRY_DELAY_SEC"
else
npm run e2e -- --app "${{ inputs.app }}" --platform ios --simulator "${{ steps.boot-simulator.outputs.simulator }}"
npm run e2e -- --app "${{ inputs.app }}" --platform ios --simulator "${{ steps.boot-simulator.outputs.simulator }}" --retry-count "$E2E_RETRY_COUNT" --retry-delay-sec "$E2E_RETRY_DELAY_SEC"
fi

e2e-android:
Expand Down Expand Up @@ -138,10 +141,13 @@ jobs:
echo "$HOME/.maestro-runner/bin" >> "$GITHUB_PATH"

- name: Run Android E2E
env:
E2E_RETRY_COUNT: "3"
E2E_RETRY_DELAY_SEC: "30"
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 35
arch: x86_64
profile: pixel_7
emulator-boot-timeout: 900
script: if [ "${{ inputs.expo }}" = "true" ]; then npm run e2e -- --app "${{ inputs.app }}" --platform android --framework expo; else npm run e2e -- --app "${{ inputs.app }}" --platform android; fi
script: if [ "${{ inputs.expo }}" = "true" ]; then npm run e2e -- --app "${{ inputs.app }}" --platform android --framework expo --retry-count "$E2E_RETRY_COUNT" --retry-delay-sec "$E2E_RETRY_DELAY_SEC"; else npm run e2e -- --app "${{ inputs.app }}" --platform android --retry-count "$E2E_RETRY_COUNT" --retry-delay-sec "$E2E_RETRY_DELAY_SEC"; fi
11 changes: 8 additions & 3 deletions .github/workflows/e2e-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,13 @@ jobs:
- name: Run iOS E2E
env:
MAESTRO_DRIVER_STARTUP_TIMEOUT: "300000"
E2E_RETRY_COUNT: "3"
E2E_RETRY_DELAY_SEC: "30"
run: |
if [ "${{ startsWith(matrix.app, 'Expo') }}" = "true" ]; then
npm run e2e -- --app "${{ matrix.app }}" --platform ios --simulator "${{ steps.boot-simulator.outputs.simulator }}" --framework expo
npm run e2e -- --app "${{ matrix.app }}" --platform ios --simulator "${{ steps.boot-simulator.outputs.simulator }}" --framework expo --retry-count "$E2E_RETRY_COUNT" --retry-delay-sec "$E2E_RETRY_DELAY_SEC"
else
npm run e2e -- --app "${{ matrix.app }}" --platform ios --simulator "${{ steps.boot-simulator.outputs.simulator }}"
npm run e2e -- --app "${{ matrix.app }}" --platform ios --simulator "${{ steps.boot-simulator.outputs.simulator }}" --retry-count "$E2E_RETRY_COUNT" --retry-delay-sec "$E2E_RETRY_DELAY_SEC"
fi

e2e-android:
Expand Down Expand Up @@ -174,10 +176,13 @@ jobs:
echo "$HOME/.maestro-runner/bin" >> "$GITHUB_PATH"

- name: Run Android E2E
env:
E2E_RETRY_COUNT: "3"
E2E_RETRY_DELAY_SEC: "30"
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 35
arch: x86_64
profile: pixel_7
emulator-boot-timeout: 900
script: if [ "${{ startsWith(matrix.app, 'Expo') }}" = "true" ]; then npm run e2e -- --app "${{ matrix.app }}" --platform android --framework expo; else npm run e2e -- --app "${{ matrix.app }}" --platform android; fi
script: if [ "${{ startsWith(matrix.app, 'Expo') }}" = "true" ]; then npm run e2e -- --app "${{ matrix.app }}" --platform android --framework expo --retry-count "$E2E_RETRY_COUNT" --retry-delay-sec "$E2E_RETRY_DELAY_SEC"; else npm run e2e -- --app "${{ matrix.app }}" --platform android --retry-count "$E2E_RETRY_COUNT" --retry-delay-sec "$E2E_RETRY_DELAY_SEC"; fi
97 changes: 92 additions & 5 deletions e2e/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,24 @@ interface CliOptions {
framework?: "expo";
simulator?: string;
maestroOnly?: boolean;
retryCount: number;
retryDelaySec: number;
}

function parseRetryCountOption(value: string): number {
const parsed = Number.parseInt(value, 10);
if (!Number.isInteger(parsed) || parsed < 1) {
throw new Error("retry-count must be an integer >= 1");
}
return parsed;
}

function parseRetryDelaySecOption(value: string): number {
const parsed = Number.parseInt(value, 10);
if (!Number.isInteger(parsed) || parsed < 0) {
throw new Error("retry-delay-sec must be an integer >= 0");
}
return parsed;
}

const program = new Command()
Expand All @@ -23,12 +41,25 @@ const program = new Command()
.requiredOption("--platform <type>", "Platform: ios or android")
.option("--framework <type>", "Framework: expo")
.option("--simulator <name>", "iOS simulator name (default: booted)")
.option("--maestro-only", "Skip build, only run test flows", false);
.option("--maestro-only", "Skip build, only run test flows", false)
.option(
"--retry-count <count>",
"Retry attempts for each Maestro execution block",
parseRetryCountOption,
1,
)
.option(
"--retry-delay-sec <seconds>",
"Delay between Maestro retries in seconds",
parseRetryDelaySecOption,
10,
);

async function main() {
const options = program.parse(process.argv).opts<CliOptions>();
const appPath = getAppPath(options.app);
const repoRoot = path.resolve(__dirname, "..");
const retryDelayMs = options.retryDelaySec * 1000;

if (!fs.existsSync(appPath)) {
console.error(`Example app not found: ${appPath}`);
Expand Down Expand Up @@ -66,7 +97,9 @@ async function main() {
// 5. Run Maestro — Phase 1: main flows
console.log("\n=== [run-maestro: phase 1] ===");
const flowsDir = path.resolve(__dirname, "flows");
await runMaestro(flowsDir, options.platform, appId);
await withRetry("run-maestro: phase 1", options.retryCount, retryDelayMs, () =>
runMaestro(flowsDir, options.platform, appId),
);

// 6. Disable release for rollback test
console.log("\n=== [disable-release] ===");
Expand All @@ -83,7 +116,12 @@ async function main() {
// 7. Run Maestro — Phase 2: rollback to binary
console.log("\n=== [run-maestro: phase 2 (rollback to binary)] ===");
const rollbackDir = path.resolve(__dirname, "flows-rollback");
await runMaestro(rollbackDir, options.platform, appId);
await withRetry(
"run-maestro: phase 2 (rollback to binary)",
options.retryCount,
retryDelayMs,
() => runMaestro(rollbackDir, options.platform, appId),
);

// 8. Prepare partial rollback: release 1.0.1 + 1.0.2 with different hashes
console.log("\n=== [prepare-bundle: partial rollback] ===");
Expand Down Expand Up @@ -124,7 +162,12 @@ async function main() {
// 9. Run Maestro — update to 1.0.2
console.log("\n=== [run-maestro: partial rollback — update to 1.0.2] ===");
const updateFlow = path.resolve(__dirname, "flows-partial-rollback/01-update-to-latest.yaml");
await runMaestro(updateFlow, options.platform, appId);
await withRetry(
"run-maestro: partial rollback — update to 1.0.2",
options.retryCount,
retryDelayMs,
() => runMaestro(updateFlow, options.platform, appId),
);

// 10. Disable only 1.0.2 → rollback target is 1.0.1 (not binary)
console.log("\n=== [disable-release: 1.0.2 only] ===");
Expand All @@ -139,7 +182,12 @@ async function main() {
// 11. Run Maestro — rollback from 1.0.2 to 1.0.1
console.log("\n=== [run-maestro: partial rollback — rollback to 1.0.1] ===");
const rollbackFlow = path.resolve(__dirname, "flows-partial-rollback/02-rollback-to-previous.yaml");
await runMaestro(rollbackFlow, options.platform, appId);
await withRetry(
"run-maestro: partial rollback — rollback to 1.0.1",
options.retryCount,
retryDelayMs,
() => runMaestro(rollbackFlow, options.platform, appId),
);

console.log("\n=== E2E tests passed ===");
} catch (error) {
Expand Down Expand Up @@ -231,6 +279,45 @@ function buildCodePushBundleIdentifier(appName: string): string {
return `com.${normalized}`;
}

async function withRetry(
label: string,
retryCount: number,
retryDelayMs: number,
action: () => Promise<void>,
): Promise<void> {
for (let attempt = 1; attempt <= retryCount; attempt += 1) {
if (retryCount > 1) {
console.log(`[retry] ${label} attempt ${attempt}/${retryCount}`);
}

try {
await action();
if (attempt > 1) {
console.log(`[retry] ${label} succeeded on attempt ${attempt}/${retryCount}`);
}
return;
} catch (error) {
if (attempt === retryCount) {
throw error;
}

const message = error instanceof Error ? error.message : String(error);
console.warn(`[retry] ${label} failed on attempt ${attempt}/${retryCount}: ${message}`);

if (retryDelayMs > 0) {
console.log(`[retry] waiting ${retryDelayMs / 1000}s before retry`);
await sleep(retryDelayMs);
}
}
}
}

function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}

function runMaestro(
flowsDir: string,
platform: "ios" | "android",
Expand Down
Loading