Compare commits

..
Author SHA1 Message Date
szdziedzic 3b836ef30c [eas-cli] Add --os-version to eas simulator 2026-10-26 16:24:00 +01:00
Stanisław Chmiela bcb5d96ea8 [cocoapods-proxy] Add Sonatype snapshot CocoaPods proxy support (#3736)
## Summary

Adds `central.sonatype.com` support to the packaged `expo-cocoapods-proxy` gem used by iOS workers.

## Change

- Adds `central.sonatype.com` to the `Pod::Downloader::Http` proxy allowlist.
- Adds spec coverage for Sonatype repository artifact URLs.

## Code Requiring This

This PR is for CocoaPods `:http` pod source downloads. It does not proxy React Native's `Net::HTTP` metadata lookup.

React Native returns Sonatype artifact URLs to CocoaPods as `:http` pod sources here:

- RNCore returns `{:http => url}` after resolving the Sonatype nightly artifact URL: https://github.com/facebook/react-native/blob/7bbb13c9be014c0e63f0deaad809d669c15325ad/packages/react-native/scripts/cocoapods/rncore.rb#L170-L173
- ReactNativeDependencies returns `{:http => url}` after resolving the Sonatype nightly artifact URL: https://github.com/facebook/react-native/blob/7bbb13c9be014c0e63f0deaad809d669c15325ad/packages/react-native/scripts/cocoapods/rndependencies.rb#L205-L208
- Hermes returns `{:http => url}` after resolving the Sonatype nightly artifact URL: https://github.com/facebook/react-native/blob/7bbb13c9be014c0e63f0deaad809d669c15325ad/packages/react-native/sdks/hermes-engine/hermes-utils.rb#L190-L193

CocoaPods/cocoapods-downloader then uses `Pod::Downloader::Http` for those `:http` params:

- CocoaPods passes the pod source params to `Downloader.for_target(...).download`: https://github.com/CocoaPods/CocoaPods/blob/1.12.1/lib/cocoapods/downloader.rb#L107-L110
- `cocoapods-downloader` maps `:http` to `Http`: https://github.com/CocoaPods/cocoapods-downloader/blob/1.6.3/lib/cocoapods-downloader.rb#L22-L30
- `Downloader.for_target` instantiates that downloader with the `:http` URL: https://github.com/CocoaPods/cocoapods-downloader/blob/1.6.3/lib/cocoapods-downloader.rb#L51-L64
- `Pod::Downloader::Http#download_file` uses curl for that URL: https://github.com/CocoaPods/cocoapods-downloader/blob/1.6.3/lib/cocoapods-downloader/http.rb#L12-L22

## Validation

- `rspec` in `packages/expo-cocoapods-proxy`: 16 examples, 0 failures
- `git diff --check`
2026-10-26 15:51:09 +01:00
10 changed files with 106 additions and 64 deletions

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
'eas-cli': minor
---
Add `--os-version` to `eas simulator` to select the iOS Simulator runtime version.
-5
View File
@@ -1,5 +0,0 @@
---
'@expo/eas-build-job': patch
---
Add types and validation for documented GitHub workflow context fields.
@@ -871,7 +871,7 @@ describe('StaticWorkflowInterpolationContextZ', () => {
});
describe('GitHub context event payload passthrough', () => {
it('preserves webhook fields and validates documented pull request details', () => {
it('accepts arbitrary fields at all event levels', () => {
const context = {
after: {},
needs: {},
@@ -895,19 +895,7 @@ describe('GitHub context event payload passthrough', () => {
},
pull_request: {
number: 123,
title: 'Fix the build',
body: null,
state: 'open',
draft: true,
merged: false,
html_url: 'https://github.com/expo/expo/pull/123',
user: { login: 'octocat', id: 1 },
labels: [{ name: 'bug', color: 'red' }],
head: { ref: 'fix-build', sha: 'abc123', repo: { name: 'expo' } },
base: { ref: 'main', sha: 'def456' },
created_at: '2026-10-01T00:00:00Z',
updated_at: '2026-10-02T00:00:00Z',
merged_at: null,
},
number: 123,
schedule: '0 0 * * *',
@@ -933,34 +921,6 @@ describe('GitHub context event payload passthrough', () => {
const parsed = StaticWorkflowInterpolationContextZ.parse(context);
expect(parsed.github?.event).toMatchObject(context.github.event);
expect(() =>
StaticWorkflowInterpolationContextZ.parse({
...context,
github: {
...context.github,
event: {
...context.github.event,
pull_request: { ...context.github.event.pull_request, title: 123 },
},
},
})
).toThrow();
});
it.each([
{ number: 123 },
{ number: 123, body: null, user: null, merged: null, merged_at: null },
])('accepts partial pull request payloads: %j', pullRequest => {
const github = {
event_name: 'pull_request',
sha: 'abc123',
ref: 'refs/heads/main',
ref_name: 'main',
ref_type: 'branch',
event: { pull_request: pullRequest },
};
expect(StaticWorkflowInterpolationContextZ.shape.github.parse(github)).toEqual(github);
});
});
-14
View File
@@ -207,7 +207,6 @@ const GitHubContextZ = z.object({
.record(z.string(), z.unknown())
.and(
z.object({
action: z.string().optional(),
label: z
.looseObject({
name: z.string(),
@@ -222,19 +221,6 @@ const GitHubContextZ = z.object({
pull_request: z
.looseObject({
number: z.number(),
title: z.string().optional(),
body: z.string().nullable().optional(),
state: z.enum(['open', 'closed']).optional(),
draft: z.boolean().optional(),
merged: z.boolean().nullable().optional(),
html_url: z.string().optional(),
user: z.looseObject({ login: z.string() }).nullable().optional(),
labels: z.array(z.looseObject({ name: z.string() })).optional(),
head: z.looseObject({ ref: z.string(), sha: z.string() }).optional(),
base: z.looseObject({ ref: z.string(), sha: z.string() }).optional(),
created_at: z.string().optional(),
updated_at: z.string().optional(),
merged_at: z.string().nullable().optional(),
})
.optional(),
number: z.number().optional(),
+13 -1
View File
@@ -46796,7 +46796,19 @@
"inputFields": [
{
"name": "deviceIdentifier",
"description": "Simulator device name or UDID to use for the session (e.g. \"iPhone 16 Pro\").",
"description": "Simulator device name or UDID to use for the session (e.g. \"iPhone 17 Pro\"). If omitted,\nthe default device of the selected iOS version is used.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "osVersion",
"description": "iOS version of the Simulator runtime, as major.minor (e.g. \"18.6\") or as a major version\n(e.g. \"26\") for the newest available release of that major. Each version runs on the macOS\nworker image that has that runtime, so the session also uses that image's Xcode. Unavailable\nversions are rejected with the list of available ones. Not every account can select a version\nyet. If omitted, the default iOS version is used.",
"type": {
"kind": "SCALAR",
"name": "String",
@@ -1223,6 +1223,54 @@ describe(Simulator, () => {
expect(mockCreateDeviceRunSessionAsync.mock.calls[0][1]).not.toHaveProperty('ios');
});
it('forwards --os-version in the iOS create options', async () => {
const { command } = createCommand([
'--platform',
'ios',
'--non-interactive',
'--os-version',
' 18.5 ',
]);
await command.runAsync();
expect(mockCreateDeviceRunSessionAsync).toHaveBeenCalledWith(
graphqlClient,
expect.objectContaining({ ios: { osVersion: '18.5' } })
);
});
it('forwards --os-version together with --device', async () => {
const { command } = createCommand([
'--platform',
'ios',
'--non-interactive',
'--device',
'iPhone 16 Pro',
'--os-version',
'26',
]);
await command.runAsync();
expect(mockCreateDeviceRunSessionAsync).toHaveBeenCalledWith(
graphqlClient,
expect.objectContaining({ ios: { deviceIdentifier: 'iPhone 16 Pro', osVersion: '26' } })
);
});
it('rejects --os-version for Android before creating a session', async () => {
const { command } = createCommand([
'--platform',
'android',
'--non-interactive',
'--os-version',
'18.5',
]);
await expect(command.runAsync()).rejects.toThrow(
'--os-version is only supported with --platform ios.'
);
expect(mockCreateDeviceRunSessionAsync).not.toHaveBeenCalled();
});
it('omits resourceClass when --resource-class is not set', async () => {
const { command } = createCommand(['--platform', 'ios', '--non-interactive']);
await command.runAsync();
@@ -108,6 +108,10 @@ export default class Simulator extends EasCommand {
description:
'Android SDK system image package to use for the emulator (e.g. "system-images;android-35-ext15;google_apis_playstore;x86_64"). Defaults to a system image chosen by the runner. Only supported with --platform android.',
}),
'os-version': Flags.string({
description:
'iOS version of the Simulator runtime, as major.minor (e.g. "18.6") or as a major version (e.g. "26") for the newest available release of that major. Unavailable versions are rejected with the list of available ones. Defaults to the server default. Only supported with --platform ios.',
}),
'build-id': Flags.string({
description: 'EAS Build to install and launch before the simulator session is ready.',
exclusive: ['build-fingerprint', 'application-archive-url', 'expo-go'],
@@ -257,6 +261,7 @@ export default class Simulator extends EasCommand {
const tags = flags.tag?.map(tag => tag.trim()).filter(tag => tag.length > 0);
const deviceIdentifier = flags.device?.trim() || undefined;
const systemImagePackage = flags['system-image']?.trim() || undefined;
const osVersion = flags['os-version']?.trim() || undefined;
const buildId = flags['build-id']?.trim() || undefined;
const buildFingerprint = flags['build-fingerprint']?.trim() || undefined;
const applicationArchiveUrlFromFlag = flags['application-archive-url']?.trim() || undefined;
@@ -303,6 +308,9 @@ export default class Simulator extends EasCommand {
if (systemImagePackage && platform !== AppPlatform.Android) {
throw new EasCommandError('--system-image is only supported with --platform android.');
}
if (osVersion && platform !== AppPlatform.Ios) {
throw new EasCommandError('--os-version is only supported with --platform ios.');
}
let egressAllow: string[] = [];
try {
egressAllow = parseEgressAllowList(flags['egress-allow'] ?? []);
@@ -379,8 +387,13 @@ export default class Simulator extends EasCommand {
networkCapture: flags['network-capture'],
...(networkCaptureFields.length ? { networkCaptureFields } : {}),
...(platform === AppPlatform.Ios
? deviceIdentifier
? { ios: { deviceIdentifier } }
? deviceIdentifier || osVersion
? {
ios: {
...(deviceIdentifier ? { deviceIdentifier } : {}),
...(osVersion ? { osVersion } : {}),
},
}
: {}
: deviceIdentifier || systemImagePackage
? {
+12 -1
View File
@@ -6613,8 +6613,19 @@ export type CreateGitHubRepositorySettingsInput = {
};
export type CreateIosDeviceRunSessionInput = {
/** Simulator device name or UDID to use for the session (e.g. "iPhone 16 Pro"). */
/**
* Simulator device name or UDID to use for the session (e.g. "iPhone 17 Pro"). If omitted,
* the default device of the selected iOS version is used.
*/
deviceIdentifier?: InputMaybe<Scalars['String']['input']>;
/**
* iOS version of the Simulator runtime, as major.minor (e.g. "18.6") or as a major version
* (e.g. "26") for the newest available release of that major. Each version runs on the macOS
* worker image that has that runtime, so the session also uses that image's Xcode. Unavailable
* versions are rejected with the list of available ones. Not every account can select a version
* yet. If omitted, the default iOS version is used.
*/
osVersion?: InputMaybe<Scalars['String']['input']>;
};
export type CreateIosSubmissionInput = {
@@ -17,6 +17,7 @@ module Pod
%r{https?://(api\.)?github\.com/},
%r{https?://sourceforge\.net/},
%r{https?://repo1\.maven\.org/},
%r{https?://central\.sonatype\.com/},
%r{https?://boostorg\.jfrog\.io/},
%r{https?://www\.sqlite\.org/},
%r{https?://download\.videolan\.org/},
@@ -142,6 +142,17 @@ RSpec.describe ExpoCocoaPodsProxy do
downloader.download_file(path.to_s)
end
it 'fetches Sonatype snapshot artifacts via proxy' do
path = Pathname.new('some/fake/path')
url = 'https://central.sonatype.com/repository/maven-snapshots/com/facebook/react/react-native-artifacts/0.82.0-SNAPSHOT/maven-metadata.xml'
opts = {}
downloader = Pod::Downloader::Http.new(path, url, opts)
expect(downloader).to receive(:download_file_via_proxy).once {}
downloader.download_file(path.to_s)
end
it 'does not proxy when url doesn\'t match an allowlist regex' do
path = Pathname.new('some/fake/path')
url = 'https://not-github.com/expo/test-repo/archive/main.tar.gz'