Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
17 changes: 17 additions & 0 deletions .github/actions/prepare-model-env/pick.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,19 @@ normalize_token() {
printf '%s' "$v"
}

# Classify an arbitrary CLI log for a caller. The verify canary hits the
# same failure modes this probe does, but later in the job — after this
# step already passed — so its reporter cannot lean on model-env-reason.
# One classifier, exposed here, keeps the two verdicts from drifting.
if [ "${1:-}" = --classify ]; then
if [ -z "${2:-}" ] || [ ! -f "$2" ]; then
echo other
exit 0
fi
classify_failure "$2"
exit 0
fi

if [ "${1:-}" = --self-test ]; then
tmp=$(mktemp)
printf '%s\n' 'HTTP 429 rate_limit_error: rate limit exceeded' >"$tmp"
Expand All @@ -62,6 +75,10 @@ if [ "${1:-}" = --self-test ]; then
[ "$(classify_failure "$tmp")" = auth ]
printf '%s\n' 'ECONNRESET connection reset by peer' >"$tmp"
[ "$(classify_failure "$tmp")" = other ]
printf '%s\n' 'API Error: 529 Overloaded. This is a server-side issue, usually temporary.' >"$tmp"
[ "$(classify_failure "$tmp")" = other ]
[ "$(bash "$0" --classify "$tmp")" = other ]
[ "$(bash "$0" --classify /nonexistent-classify-input)" = other ]
rm -f "$tmp"
[ "$(token_shape "sk-ant-oat01-aaaa")" = oat ]
[ "$(token_shape "sk-ant-api03-aaaa")" = api ]
Expand Down
69 changes: 59 additions & 10 deletions .github/workflows/agent-commands.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1089,17 +1089,35 @@ jobs:
else
canary_prompt="Write the single word ok to .verify-out/.permcheck. Say nothing else."
fi
set +e
claude -p "$canary_prompt" \
--model "$VERIFY_MODEL" \
--disallowedTools "$DENIED" --strict-mcp-config \
--allowedTools "$ALLOWED" > "$RUNNER_TEMP/permcheck.log" 2>&1 || true
--allowedTools "$ALLOWED" > "$RUNNER_TEMP/permcheck.log" 2>&1
canary_rc=$?
set -e
if [ ! -f .verify-out/.permcheck ]; then
# Also record WHICH failure this was, for the thread comment.
# The reporter's cause line comes from model-env-reason, and the
# credential probe PASSED by the time this runs — so without a
# verdict of its own, a canary death posts a bare "failed" with
# no cause (2026-08-18, PR 46816: a 529 between the probe and
# the canary). The verdict is grounded, not guessed: exit 0
# with no file means the CLI ran and the rule did not match;
# non-zero means the CLI itself failed, and the probe's own
# classifier names the flavor from the kept output.
if [ "$canary_rc" -eq 0 ]; then
echo permission > "$RUNNER_TEMP/canary-reason"
else
bash .github/actions/prepare-model-env/pick.sh --classify "$RUNNER_TEMP/permcheck.log" > "$RUNNER_TEMP/canary-reason" || true
fi
echo "::error::Preflight canary did not write .verify-out/.permcheck, so the run stopped before spending a sandbox. This is EITHER a file-permission rule that does not match OR the CLI failing to run at all (credential, model access, outage). The CLI output below says which:"
sed -n '1,40p' "$RUNNER_TEMP/permcheck.log" || true
echo "::error::--allowedTools was: $ALLOWED"
exit 1
fi
if [ "$FIX_MODE" = "true" ] && [ ! -f .permcheck-checkout ]; then
echo permission > "$RUNNER_TEMP/canary-reason"
echo "::error::Preflight canary wrote .verify-out but not the checkout root, so the checkout-wide Edit rule is not matching — the agent would do its work and then fail to mirror it. Stopped before spending a sandbox. The CLI output:"
sed -n '1,40p' "$RUNNER_TEMP/permcheck.log" || true
echo "::error::--allowedTools was: $ALLOWED"
Expand Down Expand Up @@ -1736,12 +1754,27 @@ jobs:
agent=$(git rev-parse HEAD)
echo "origin/main moved under the run (${base:0:12} -> ${tip:0:12}); replaying ${agent:0:12} onto it"
git checkout --detach origin/main
if ! git cherry-pick "$agent"; then
# cherry-pick CREATES a commit, so it needs the same committer
# identity the publish commit got — without it, git dies with
# "empty ident name" and the first moved-main replay in the
# wild was misreported as "conflict: unknown" (run 32167073927).
if ! pick_out=$(git -c user.name='expo-bot' -c user.email='expo-bot@users.noreply.github.com' \
cherry-pick "$agent" 2>&1); then
conflicted=$(git diff --name-only --diff-filter=U | tr '\n' ' ')
git cherry-pick --abort >/dev/null 2>&1 || true
echo "::error::could not replay the publish commit onto origin/main (conflict: ${conflicted:-unknown})."
echo "ALIGN_FAIL=replay" >> "$RUNNER_TEMP/align.env"
# Only a failure with unmerged paths is a conflict. Anything
# else is git refusing for its own reasons — show its words
# instead of diagnosing a conflict that did not happen.
if [ -n "$conflicted" ]; then
echo "::error::could not replay the publish commit onto origin/main (conflict: $conflicted)."
else
echo "::error::could not replay the publish commit onto origin/main (not a content conflict — git said):"
printf '%s\n' "$pick_out"
fi
return 1
fi
printf '%s\n' "$pick_out"
return 0
}
local_workflows_tree() {
Expand Down Expand Up @@ -1808,6 +1841,12 @@ jobs:
if [ "$fail_kind" = "stale_fork" ]; then
gh issue comment "$ISSUE_NUMBER" --body "⛔ \`$command_name\` prepared the $change_name but the bot fork's workflow files do not match current \`main\`, and the dedicated sync job is not running. Land on \`main\` (or re-run \`sync-expo-bot-fork\`) and re-trigger. The outcome comment still stands. [Run log]($RUN_URL)"
echo "::error::stale fork with no in-flight sync; refusing to push."
elif [ "$fail_kind" = "replay" ]; then
# Without its own branch this fell through to the workflow-tree
# message below, which told the thread a fork-sync story about
# a replay failure (run 32167073927).
gh issue comment "$ISSUE_NUMBER" --body "⛔ \`$command_name\` prepared the $change_name, but \`main\` moved under the run and the change could not be replayed onto the new tip. Re-trigger \`$command_name\` to regenerate it against current \`main\`. The outcome comment still stands and describes the change. [Run log]($RUN_URL)"
echo "::error::replay onto moved origin/main failed; refusing to push."
else
gh issue comment "$ISSUE_NUMBER" --body "⛔ \`$command_name\` prepared the $change_name but could not line its workflow files up with the bot fork in time (the dedicated sync job on \`expo/expo\` main is what fast-forwards the fork). Re-run once that job has finished. The outcome comment still stands. [Run log]($RUN_URL)"
echo "::error::could not match $fork .github/workflows to this commit; refusing to push."
Expand Down Expand Up @@ -2101,16 +2140,26 @@ jobs:
cancelled) outcome="was cancelled" ;;
*) outcome="failed" ;;
esac
cause=""
# Two sources for the cause, one vocabulary. model-env-reason
# covers a credential probe that failed; canary-reason covers a
# probe that PASSED followed by a preflight canary that did not —
# a window a 529 fell into on 2026-08-18 (PR 46816), which posted
# a bare "failed" with no cause and no hint that a plain retry
# would work.
reason=""
if [ "$MODEL_ENV" = "failure" ]; then
reason=$(cat "$RUNNER_TEMP/model-env-reason" 2>/dev/null || echo unprobed)
case "$reason" in
auth) cause=" The cause is its model credential, which was rejected — a maintainer needs to take a look." ;;
usage) cause=" The cause is a temporary model usage limit; nothing is misconfigured, so trying again later should work." ;;
other) cause=" The cause is a failure reaching the model — a network fault, a timeout, or a provider outage." ;;
*) cause=" It failed before reaching the model, so this is a workflow problem rather than a credential one." ;;
esac
elif [ -s "$RUNNER_TEMP/canary-reason" ]; then
reason=$(cat "$RUNNER_TEMP/canary-reason")
fi
case "$reason" in
"") cause="" ;;
auth) cause=" The cause is its model credential, which was rejected — a maintainer needs to take a look." ;;
usage) cause=" The cause is a temporary model usage limit; nothing is misconfigured, so trying again later should work." ;;
other) cause=" The cause is a failure reaching the model — a network fault, a timeout, or a provider outage; nothing is misconfigured, so trying again later should work." ;;
permission) cause=" The cause is the workflow's file-permission rules: the preflight write check was refused, so a maintainer needs to fix the \`--allowedTools\` rules first — a plain retry will fail the same way." ;;
*) cause=" It failed before reaching the model, so this is a workflow problem rather than a credential one." ;;
esac
# Point at the salvage when there is one. An interrupted run often
# established real things before it died, and a maintainer should be
# told where they are rather than left to assume nothing survived.
Expand Down
1 change: 1 addition & 0 deletions apps/observe-tester/app/(tabs)/examples/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default function ExamplesLayout() {
<Stack.Screen name="nested-stack" options={{ headerShown: false }} />
<Stack.Screen name="preloaded" options={{ headerShown: false }} />
<Stack.Screen name="modals" options={{ headerShown: false }} />
<Stack.Screen name="event-flood" options={{ title: 'Event flood' }} />
</Stack>
);
}
85 changes: 85 additions & 0 deletions apps/observe-tester/app/(tabs)/examples/event-flood.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import AppMetrics from 'expo-app-metrics';
import { Observe } from 'expo-observe';
import { useState } from 'react';
import { Platform, ScrollView, StyleSheet, Text } from 'react-native';

import { Button } from '@/components/Button';
import { useTheme } from '@/utils/theme';

export default function EventFlood() {
const theme = useTheme();
const [status, setStatus] = useState<string | null>(null);

function logFlood(count: number) {
const startedAt = performance.now();

for (let index = 0; index < count; index++) {
Observe.logEvent('stress.driver_event', {
severity: 'info',
body: 'Driver telemetry update received',
attributes: {
driverId: 'driver_48291',
vehicleId: 'vehicle_731',
tripId: 'trip_2026_08_14_1842',
mode: 'delivery',
isOnline: true,
speedKph: 42.7,
location: {
latitude: 37.7751,
longitude: -122.4193,
accuracyMeters: 8.4,
},
route: ['warehouse', 'pickup', 'dropoff'],
index,
},
});
}

setStatus(
`Logged ${count.toLocaleString()} events in ${Math.round(performance.now() - startedAt).toLocaleString()} ms`
);
}

async function clearStoredEntries() {
try {
await AppMetrics.clearStoredEntries();
setStatus('Cleared stored entries');
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setStatus(`Failed to clear stored entries: ${message}`);
}
}

return (
<ScrollView
style={[styles.container, { backgroundColor: theme.background.screen }]}
contentContainerStyle={styles.content}>
<Text style={[styles.description, { color: theme.text.secondary }]}>
Log a burst of driver telemetry events synchronously to reproduce high-volume integrations.
</Text>
<Button title="Log 100 events" onPress={() => logFlood(100)} />
<Button title="Log 1,000 events" onPress={() => logFlood(1_000)} />
<Button title="Log 5,000 events" onPress={() => logFlood(5_000)} />
<Button title="Clear stored entries" onPress={clearStoredEntries} theme="secondary" />
{status ? <Text style={[styles.status, { color: theme.text.default }]}>{status}</Text> : null}
</ScrollView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
padding: 20,
paddingBottom: Platform.select({ ios: 30, android: 150 }),
},
description: {
fontSize: 14,
marginBottom: 20,
},
status: {
fontSize: 14,
fontWeight: '600',
},
});
5 changes: 5 additions & 0 deletions apps/observe-tester/app/(tabs)/examples/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export default function ExamplesIndex() {
description="modal / formSheet / pageSheet presentations"
onPress={() => router.push('/examples/modals')}
/>
<Button
title="Event flood"
description="Generate high-volume log events"
onPress={() => router.push('/examples/event-flood')}
/>
</ScrollView>
);
}
Expand Down
8 changes: 4 additions & 4 deletions docs/pages/guides/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@ import { CpuChip01Icon } from '@expo/styleguide-icons/outline/CpuChip01Icon';

This section contains information about the development with Expo and Expo Application Services (EAS):

## <span className="inline-flex items-center gap-2"><CodeSquare01Icon />Development process</span>
## <span className="inline-flex items-center gap-2"><CodeSquare01Icon aria-hidden="true" />Development process</span>

Learn about the process of [building an app with Expo](/workflow/overview/) to help understand the mental model of the core development loop. This section also dives into additional configurations and workflows you may require during the development process to help you develop, deploy, and maintain your app. It contains in-depth information about [app config](/workflow/configuration/), [permissions](/guides/permissions/), [universal links](/linking/into-your-app/), [custom native code](/workflow/continuous-native-generation/), [web](/workflow/web/), and more.

## <span className="inline-flex items-center gap-2"><RouterLogo />Expo Router</span>
## <span className="inline-flex items-center gap-2"><RouterLogo aria-hidden="true" />Expo Router</span>

Learn about using different navigation functionalities from the [Expo Router](/router/introduction/) library. It also covers a comprehensive [Hooks API](/versions/latest/sdk/router/#hooks) that the library provides and other aspects of navigation such as [Authentication](/router/advanced/authentication/), [Redirects](/router/reference/redirects/), [Testing](/router/reference/testing/), and more.

## <span className="inline-flex items-center gap-2"><CpuChip01Icon />Expo Modules API</span>
## <span className="inline-flex items-center gap-2"><CpuChip01Icon aria-hidden="true" />Expo Modules API</span>

Learn how to add and use native modules in your app using [Expo Modules API](/modules/overview/).

## <span className="inline-flex items-center gap-2"><GraduationHat02DuotoneIcon />Tutorials</span>
## <span className="inline-flex items-center gap-2"><GraduationHat02DuotoneIcon aria-hidden="true" />Tutorials</span>

If you're looking for step-by-step tutorials for Expo and EAS, see the [Tutorial section](/tutorial/overview/) which includes comprehensive tutorials for both [building apps with Expo](/tutorial/introduction/) and [using EAS services](/tutorial/eas/introduction/).

Expand Down
6 changes: 3 additions & 3 deletions docs/pages/router/advanced/authentication-rewrites.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ It's common to restrict specific routes to users who are not authenticated. This
[
'app/sign-in.tsx',
<span>
Always accessible <LockUnlocked01Icon className="mb-1 inline" />
Always accessible <LockUnlocked01Icon aria-hidden="true" className="mb-1 inline" />
</span>,
],
['app/(app)/_layout.tsx', <span>Protects child routes</span>],
[
'app/(app)/index.tsx',
<span>
Requires authorization <Lock01Icon className="mb-1 inline" />
Requires authorization <Lock01Icon aria-hidden="true" className="mb-1 inline" />
</span>,
],
]}
Expand Down Expand Up @@ -305,7 +305,7 @@ Another common pattern is to render a sign-in modal over the top of the app. Thi
[
'app/(app)/(root)/index.tsx',
<span>
Requires authorization <Lock01Icon className="mb-1 inline" />
Requires authorization <Lock01Icon aria-hidden="true" className="mb-1 inline" />
</span>,
],
]}
Expand Down
6 changes: 3 additions & 3 deletions docs/pages/router/advanced/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ With Expo Router, all routes are always defined and accessible. You can use runt
[
'src/app/sign-in.tsx',
<span>
Always accessible <LockUnlocked01Icon className="mb-1 inline" />
Always accessible <LockUnlocked01Icon aria-hidden="true" className="mb-1 inline" />
</span>,
],
[
'src/app/(app)/_layout.tsx',
<span>
Requires authorization <Lock01Icon className="mb-1 inline" />
Requires authorization <Lock01Icon aria-hidden="true" className="mb-1 inline" />
</span>,
],
['src/app/(app)/index.tsx', <span>Should be protected by the (app)/_layout</span>],
Expand Down Expand Up @@ -335,7 +335,7 @@ Another common pattern is to render a sign-in modal over the top of the app. Thi
[
'src/app/(app)/(root)/index.tsx',
<span>
Requires authorization <Lock01Icon className="mb-1 inline" />
Requires authorization <Lock01Icon aria-hidden="true" className="mb-1 inline" />
</span>,
],
]}
Expand Down
6 changes: 3 additions & 3 deletions docs/pages/router/basics/common-navigation-patterns.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -203,21 +203,21 @@ For example, consider the following navigation tree in which you have a bottom t
[
'src/app/(tabs)/index.tsx',
<span>
Protected <Lock01Icon className="mb-1 inline" />
Protected <Lock01Icon aria-hidden="true" className="mb-1 inline" />
</span>,
],
[
'src/app/(tabs)/settings.tsx',
<span>
Protected <Lock01Icon className="mb-1 inline" />
Protected <Lock01Icon aria-hidden="true" className="mb-1 inline" />
</span>,
],
['src/app/sign-in.tsx'],
['src/app/create-account.tsx'],
[
'src/app/modal.tsx',
<span>
Protected <Lock01Icon className="mb-1 inline" />
Protected <Lock01Icon aria-hidden="true" className="mb-1 inline" />
</span>,
],
]}
Expand Down
5 changes: 4 additions & 1 deletion docs/pages/versions/unversioned/sdk/safe-area-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ function HookComponent() {

<APIBoxSectionHeader text="Returns" />

<CornerDownRightIcon className="icon-sm text-icon-secondary mr-2 inline-block align-middle" />
<CornerDownRightIcon
aria-hidden="true"
className="icon-sm text-icon-secondary mr-2 inline-block align-middle"
/>
[`EdgeInsets`](#edgeinsets)

</PaddedAPIBox>
Expand Down
5 changes: 4 additions & 1 deletion docs/pages/versions/v54.0.0/sdk/safe-area-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ function HookComponent() {

<APIBoxSectionHeader text="Returns" />

<CornerDownRightIcon className="icon-sm text-icon-secondary mr-2 inline-block align-middle" />
<CornerDownRightIcon
aria-hidden="true"
className="icon-sm text-icon-secondary mr-2 inline-block align-middle"
/>
[`EdgeInsets`](#edgeinsets)

</PaddedAPIBox>
Expand Down
5 changes: 4 additions & 1 deletion docs/pages/versions/v55.0.0/sdk/safe-area-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ function HookComponent() {

<APIBoxSectionHeader text="Returns" />

<CornerDownRightIcon className="icon-sm text-icon-secondary mr-2 inline-block align-middle" />
<CornerDownRightIcon
aria-hidden="true"
className="icon-sm text-icon-secondary mr-2 inline-block align-middle"
/>
[`EdgeInsets`](#edgeinsets)

</PaddedAPIBox>
Expand Down
5 changes: 4 additions & 1 deletion docs/pages/versions/v56.0.0/sdk/safe-area-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ function HookComponent() {

<APIBoxSectionHeader text="Returns" />

<CornerDownRightIcon className="icon-sm text-icon-secondary mr-2 inline-block align-middle" />
<CornerDownRightIcon
aria-hidden="true"
className="icon-sm text-icon-secondary mr-2 inline-block align-middle"
/>
[`EdgeInsets`](#edgeinsets)

</PaddedAPIBox>
Expand Down
Loading
Loading