feat(supported-version): add service_preferences and support for php-fpm and nginx (#255)

This commit is contained in:
Damien Retzinger
2026-05-17 15:51:05 -04:00
parent 8e82fcc893
commit e89f6ad2e0
16 changed files with 642 additions and 47 deletions
+42
View File
@@ -18,6 +18,7 @@ See the [action.yml](./action.yml)
| custom_versions | The versions you want to support, as a comma-separated string, i.e. 'magento/project-community-edition:2.3.7-p3, magento/project-community-edition:2.4.2-p2' | false | '' |
| recent_time_frame | The time frame (from today) used when `kind` is `recent`. Combination of years (y), months (m), and days (d), e.g. `2y 2m 2d`. | false | '2y' |
| include_services | Whether to include a `services` key in each matrix entry with GitHub Actions service container configurations for MySQL, search engine, RabbitMQ, and cache. | false | 'true' |
| service_preferences | Comma-separated list of service implementations to prefer (e.g. `elasticsearch,valkey`). See [Service preferences](#service-preferences). | false | '' |
## Kinds
- `currently-supported` - The currently supported Magento Open Source versions by Adobe.
@@ -32,6 +33,47 @@ See the [action.yml](./action.yml)
- `mage-os`
- `magento-open-source` (default)
## Service preferences
When `include_services: true` (the default), each matrix entry is enriched with a `services` map. Some tiers of services (for example, search) have more than one valid implementation across the supported Magento versions:
- **search**: `opensearch` or `elasticsearch`
- **cache**: `valkey` or `redis`
By default the action picks `opensearch` over `elasticsearch` and `valkey` over `redis` wherever both are available for the matrix entry's Magento version. `service_preferences` lets the caller override that default pick by naming the implementation they want.
Tiers without a preference fall back to the per-version default pick. Your preferences are **selective**, not **exclusive**.
### Format
A comma-separated list of service implementation names. Whitespace around names is tolerated.
```yml
with:
service_preferences: elasticsearch,valkey
```
### Accepted names
| Name | Tier |
|------------------|--------|
| `mysql` | db |
| `elasticsearch` | search |
| `opensearch` | search |
| `rabbitmq` | queue |
| `redis` | cache |
| `valkey` | cache |
### Example
```yml
- uses: graycoreio/github-actions-magento2/supported-version@v8.0.0 # x-release-please-version
id: supported-version
with:
kind: currently-supported
service_preferences: opensearch,valkey
```
## Usage
```yml
+5
View File
@@ -27,6 +27,11 @@ inputs:
default: "true"
description: "Whether to include a `services` key in each matrix entry with GitHub Actions service configurations."
service_preferences:
required: false
default: ""
description: "Comma-separated list of service implementations to prefer (e.g. `elasticsearch,valkey`). Each name overrides the per-tier default implementation pick. Errors on unknown names, tier collisions (two names sharing a tier), or implementations not supported by every matrix entry for your selected kind."
outputs:
matrix:
description: "The Github Actions matrix of software technologies required to run Magento."
+29 -25
View File
File diff suppressed because one or more lines are too long
+17 -2
View File
@@ -3,6 +3,7 @@ import { validateKind } from './kind/validate-kinds';
import { getMatrixForKind } from './matrix/get-matrix-for-kind';
import { validateProject } from "./project/validate-projects";
import { buildServicesForEntry } from "./services/build-services";
import { parseServicePreferences, validatePreferencesAgainstMatrix } from "./services/preferences";
export async function run(): Promise<void> {
@@ -12,19 +13,33 @@ export async function run(): Promise<void> {
const project = core.getInput("project");
const recent_time_frame = core.getInput("recent_time_frame");
const include_services = core.getInput("include_services") === "true";
const service_preferences_raw = core.getInput("service_preferences");
validateProject(<any>project)
validateKind(<any>kind, customVersions ? customVersions.split(',') : undefined);
const preferences = parseServicePreferences(service_preferences_raw);
const hasPreferences = Object.keys(preferences).length > 0;
if (!include_services && hasPreferences) {
throw new Error(
'service_preferences cannot be combined with include_services: false. Set include_services: true or clear service_preferences.'
);
}
let matrix = getMatrixForKind(kind, project, customVersions, recent_time_frame);
if (include_services) {
if (hasPreferences) {
validatePreferencesAgainstMatrix(preferences, matrix.include);
}
const workspace = process.env.GITHUB_WORKSPACE || '';
matrix = {
magento: matrix.magento,
include: matrix.include.map((entry) => ({
...entry,
services: buildServicesForEntry(entry)
services: buildServicesForEntry(entry, preferences, workspace)
}))
};
}
@@ -36,4 +51,4 @@ export async function run(): Promise<void> {
}
}
run()
run()
@@ -3,6 +3,7 @@ export interface ServiceConfig {
env?: Record<string, string>;
ports?: string[];
options?: string;
volumes?: string[];
}
export interface Services {
@@ -223,13 +223,17 @@ describe('buildServicesForEntry', () => {
describe('complete service output', () => {
it('should build all services when all are available', () => {
const entry = createTestEntry();
const services = buildServicesForEntry(entry);
const services = buildServicesForEntry(entry, {}, '/runner/ws');
expect(Object.keys(services)).toHaveLength(4);
expect(Object.keys(services).sort()).toEqual(
['mysql', 'nginx', 'opensearch', 'php-fpm', 'rabbitmq', 'valkey']
);
expect(services.mysql).toBeDefined();
expect(services.opensearch).toBeDefined();
expect(services.rabbitmq).toBeDefined();
expect(services.valkey).toBeDefined();
expect(services.nginx).toBeDefined();
expect(services['php-fpm']).toBeDefined();
});
it('should handle entry with minimal services', () => {
@@ -239,11 +243,123 @@ describe('buildServicesForEntry', () => {
opensearch: '',
rabbitmq: '',
redis: '',
valkey: ''
valkey: '',
nginx: '',
php: ''
});
const services = buildServicesForEntry(entry);
expect(Object.keys(services)).toHaveLength(0);
});
});
describe('web tier (nginx + php-fpm)', () => {
it('emits both nginx and php-fpm when both data points are present', () => {
const entry = createTestEntry();
const services = buildServicesForEntry(entry, {}, '/runner/ws');
expect(services.nginx).toBeDefined();
expect(services['php-fpm']).toBeDefined();
});
it('uses the entry.nginx image for nginx', () => {
const entry = createTestEntry({ nginx: 'nginx:1.27-alpine' });
const services = buildServicesForEntry(entry, {}, '/runner/ws');
expect(services.nginx.image).toBe('nginx:1.27-alpine');
});
it('composes the php-fpm image from entry.php using mappia', () => {
const entry = createTestEntry({ php: '8.2' });
const services = buildServicesForEntry(entry, {}, '/runner/ws');
expect(services['php-fpm'].image).toBe('mappia/magento-php:fpm-alpine8.2');
});
it('mounts the runner workspace at /var/www/html on both services', () => {
const entry = createTestEntry();
const services = buildServicesForEntry(entry, {}, '/home/runner/work/foo/foo');
expect(services.nginx.volumes).toEqual(['/home/runner/work/foo/foo:/var/www/html']);
expect(services['php-fpm'].volumes).toEqual(['/home/runner/work/foo/foo:/var/www/html']);
});
it('exposes port 80 on nginx with the nginx -t healthcheck', () => {
const entry = createTestEntry();
const services = buildServicesForEntry(entry, {}, '/runner/ws');
expect(services.nginx.ports).toEqual(['80:80']);
expect(services.nginx.options).toContain('nginx -t');
});
it('skips both when entry.nginx is empty (they emit together or not at all)', () => {
const entry = createTestEntry({ nginx: '' });
const services = buildServicesForEntry(entry, {}, '/runner/ws');
expect(services.nginx).toBeUndefined();
expect(services['php-fpm']).toBeUndefined();
});
it('skips both when entry.php is empty (they emit together or not at all)', () => {
const entry = createTestEntry({ php: '' });
const services = buildServicesForEntry(entry, {}, '/runner/ws');
expect(services.nginx).toBeUndefined();
expect(services['php-fpm']).toBeUndefined();
});
});
describe('with service preferences', () => {
it('uses elasticsearch when search preference is elasticsearch, even if opensearch is available', () => {
const entry = createTestEntry();
const services = buildServicesForEntry(entry, { search: 'elasticsearch' });
expect(services.elasticsearch).toBeDefined();
expect(services.elasticsearch.image).toBe('elasticsearch:8.11.4');
expect(services.opensearch).toBeUndefined();
});
it('uses opensearch when search preference is opensearch (matches default)', () => {
const entry = createTestEntry();
const services = buildServicesForEntry(entry, { search: 'opensearch' });
expect(services.opensearch).toBeDefined();
expect(services.elasticsearch).toBeUndefined();
});
it('uses redis when cache preference is redis, even if valkey is available', () => {
const entry = createTestEntry();
const services = buildServicesForEntry(entry, { cache: 'redis' });
expect(services.redis).toBeDefined();
expect(services.redis.image).toBe('redis:7.2');
expect(services.valkey).toBeUndefined();
});
it('applies preferences across multiple tiers independently', () => {
const entry = createTestEntry();
const services = buildServicesForEntry(entry, { search: 'elasticsearch', cache: 'redis' });
expect(services.elasticsearch).toBeDefined();
expect(services.redis).toBeDefined();
expect(services.opensearch).toBeUndefined();
expect(services.valkey).toBeUndefined();
});
it('falls back to default-pick for tiers without a preference', () => {
const entry = createTestEntry();
const services = buildServicesForEntry(entry, { search: 'elasticsearch' });
expect(services.elasticsearch).toBeDefined();
expect(services.valkey).toBeDefined();
});
it('treats single-implementation-tier preferences as no-ops', () => {
const entry = createTestEntry();
const withPref = buildServicesForEntry(entry, { db: 'mysql', queue: 'rabbitmq' });
const withoutPref = buildServicesForEntry(entry);
expect(withPref).toEqual(withoutPref);
});
});
});
@@ -5,8 +5,11 @@ import {
opensearchConfig,
rabbitmqConfig,
redisConfig,
valkeyConfig
valkeyConfig,
buildNginxConfig,
buildPhpFpmConfig
} from './service-config';
import { ServicePreferences } from './preferences';
interface SearchEngineChoice {
type: 'opensearch' | 'elasticsearch';
@@ -19,10 +22,22 @@ interface CacheChoice {
}
/**
* Determines which search engine to use for a matrix entry.
* Prefers opensearch over elasticsearch.
* Picks the search engine for a matrix entry. Honors caller's `service_preferences`
* when set; otherwise prefers opensearch over elasticsearch.
*/
function getSearchEngineChoice(entry: PackageMatrixVersion): SearchEngineChoice | null {
const getSearchEngineChoice = (entry: PackageMatrixVersion, preference?: string): SearchEngineChoice | null => {
if (preference === 'opensearch') {
if (entry.opensearch && entry.opensearch.trim() !== '') {
return { type: 'opensearch', image: entry.opensearch };
}
return null;
}
if (preference === 'elasticsearch') {
if (entry.elasticsearch && entry.elasticsearch.trim() !== '') {
return { type: 'elasticsearch', image: entry.elasticsearch };
}
return null;
}
if (entry.opensearch && entry.opensearch.trim() !== '') {
return { type: 'opensearch', image: entry.opensearch };
}
@@ -33,10 +48,22 @@ function getSearchEngineChoice(entry: PackageMatrixVersion): SearchEngineChoice
}
/**
* Determines which cache to use for a matrix entry.
* Prefers valkey over redis.
* Picks the cache for a matrix entry. Honors caller's `service_preferences`
* when set; otherwise prefers valkey over redis.
*/
function getCacheChoice(entry: PackageMatrixVersion): CacheChoice | null {
const getCacheChoice = (entry: PackageMatrixVersion, preference?: string): CacheChoice | null => {
if (preference === 'valkey') {
if (entry.valkey && entry.valkey.trim() !== '') {
return { type: 'valkey', image: entry.valkey };
}
return null;
}
if (preference === 'redis') {
if (entry.redis && entry.redis.trim() !== '') {
return { type: 'redis', image: entry.redis };
}
return null;
}
if (entry.valkey && entry.valkey.trim() !== '') {
return { type: 'valkey', image: entry.valkey };
}
@@ -47,18 +74,25 @@ function getCacheChoice(entry: PackageMatrixVersion): CacheChoice | null {
}
/**
* Builds the services object for a single matrix entry.
* Builds the services object for a single matrix entry. Emits every
* tier the entry has data for: mysql, search (opensearch/elasticsearch),
* queue (rabbitmq), cache (valkey/redis), and web (nginx + php-fpm
* together). The web tier requires `workspace` so the volume mount
* has a real path; if the entry lacks either nginx or php data the
* web tier is skipped entirely (they're coupled — emit both or neither).
*/
export function buildServicesForEntry(entry: PackageMatrixVersion): Services {
export const buildServicesForEntry = (
entry: PackageMatrixVersion,
preferences: ServicePreferences = {},
workspace: string = ''
): Services => {
const services: Services = {};
// MySQL is always included if present
if (entry.mysql && entry.mysql.trim() !== '') {
services.mysql = mysqlConfig.getConfig(entry.mysql);
}
// Search engine: prefer opensearch over elasticsearch
const searchEngine = getSearchEngineChoice(entry);
const searchEngine = getSearchEngineChoice(entry, preferences.search);
if (searchEngine) {
if (searchEngine.type === 'opensearch') {
services.opensearch = opensearchConfig.getConfig(searchEngine.image);
@@ -67,13 +101,11 @@ export function buildServicesForEntry(entry: PackageMatrixVersion): Services {
}
}
// RabbitMQ
if (entry.rabbitmq && entry.rabbitmq.trim() !== '') {
services.rabbitmq = rabbitmqConfig.getConfig(entry.rabbitmq);
}
// Cache: prefer valkey over redis
const cache = getCacheChoice(entry);
const cache = getCacheChoice(entry, preferences.cache);
if (cache) {
if (cache.type === 'valkey') {
services.valkey = valkeyConfig.getConfig(cache.image);
@@ -82,5 +114,12 @@ export function buildServicesForEntry(entry: PackageMatrixVersion): Services {
}
}
const nginxImage = (entry.nginx || '').trim();
const phpVersion = String(entry.php ?? '').trim();
if (nginxImage !== '' && phpVersion !== '') {
services.nginx = buildNginxConfig(nginxImage, workspace);
services['php-fpm'] = buildPhpFpmConfig(phpVersion, workspace);
}
return services;
}
}
@@ -0,0 +1,3 @@
export { Tier, ServicePreferences, KNOWN_SERVICE_NAMES, tierFor } from './tier-map';
export { parseServicePreferences } from './parse-service-preferences';
export { validatePreferencesAgainstMatrix } from './validate-preferences-against-matrix';
@@ -0,0 +1,50 @@
import { parseServicePreferences } from './parse-service-preferences';
describe('parseServicePreferences', () => {
it('returns an empty map when input is empty', () => {
expect(parseServicePreferences('')).toEqual({});
});
it('returns an empty map for whitespace-only input', () => {
expect(parseServicePreferences(' ')).toEqual({});
});
it('maps a single name to its tier', () => {
expect(parseServicePreferences('opensearch')).toEqual({ search: 'opensearch' });
});
it('maps two names in different tiers', () => {
expect(parseServicePreferences('elasticsearch,valkey')).toEqual({
search: 'elasticsearch',
cache: 'valkey',
});
});
it('accepts single-implementation-tier names as no-op-like preferences', () => {
expect(parseServicePreferences('mysql,rabbitmq')).toEqual({
db: 'mysql',
queue: 'rabbitmq',
});
});
it('tolerates whitespace around names', () => {
expect(parseServicePreferences(' opensearch , valkey ')).toEqual({
search: 'opensearch',
cache: 'valkey',
});
});
it('throws on unknown service name', () => {
expect(() => parseServicePreferences('foobar')).toThrowError(/unknown service "foobar"/);
});
it('throws on a collision in the search tier', () => {
expect(() => parseServicePreferences('elasticsearch,opensearch')).toThrowError(
/collision in tier "search"/
);
});
it('throws on a collision in the cache tier', () => {
expect(() => parseServicePreferences('redis,valkey')).toThrowError(/collision in tier "cache"/);
});
});
@@ -0,0 +1,32 @@
import { ServicePreferences, SERVICE_TIER_MAP, KNOWN_SERVICE_NAMES } from './tier-map';
/**
* Parses the comma-separated `service_preferences` input into a
* tier-to-implementation map. Throws on unknown service names or when
* two names collide in the same tier.
*/
export const parseServicePreferences = (raw: string): ServicePreferences => {
const trimmed = (raw || '').trim();
if (trimmed === '') return {};
const names = trimmed.split(',').map(s => s.trim()).filter(s => s !== '');
const preferences: ServicePreferences = {};
for (const name of names) {
const tier = SERVICE_TIER_MAP[name];
if (!tier) {
throw new Error(
`service_preferences: unknown service "${name}". Known services: ${KNOWN_SERVICE_NAMES.join(', ')}`
);
}
const existing = preferences[tier];
if (existing) {
throw new Error(
`service_preferences: collision in tier "${tier}" — both "${existing}" and "${name}" specified`
);
}
preferences[tier] = name;
}
return preferences;
}
@@ -0,0 +1,18 @@
import { tierFor } from './tier-map';
describe('tierFor', () => {
it.each([
['mysql', 'db'],
['elasticsearch', 'search'],
['opensearch', 'search'],
['rabbitmq', 'queue'],
['redis', 'cache'],
['valkey', 'cache'],
])('maps %s to %s', (name, tier) => {
expect(tierFor(name)).toBe(tier);
});
it('returns undefined for unknown names', () => {
expect(tierFor('foobar')).toBeUndefined();
});
});
@@ -0,0 +1,63 @@
/**
* A category of services that Magento can choose between. Each tier
* has one or more concrete implementations (e.g. the `search` tier
* resolves to either `opensearch` or `elasticsearch`).
*/
export type Tier = 'db' | 'search' | 'queue' | 'cache';
/**
* Caller-expressed preference of "which implementation to use for which tier."
* Produced by `parseServicePreferences` from the `service_preferences`
* action input, then threaded into `buildServicesForEntry` to override
* the per-tier default pick. Partial because callers only need to name
* the tiers they care about; unset tiers fall back to the version's
* default implementation.
*/
export type ServicePreferences = Partial<Record<Tier, string>>;
/**
* Reverse lookup: which tier does each known service implementation
* belong to. The keys of this map define the closed set of legal
* service names in `service_preferences`; anything not listed here is
* rejected as an unknown service. nginx and php-fpm are intentionally
* absent — they're complementary (the `web` tier emits both together),
* not alternatives, so picking one via preferences would be meaningless.
*/
export const SERVICE_TIER_MAP: Record<string, Tier> = {
mysql: 'db',
elasticsearch: 'search',
opensearch: 'search',
rabbitmq: 'queue',
redis: 'cache',
valkey: 'cache',
};
/**
* Forward lookup: the implementations available within each tier,
* ordered by default preference (first entry wins when no caller
* preference is supplied). Used by the validator to render
* "supported: a, b" lists in compatibility errors and by
* `buildServicesForEntry` to know what to fall back to.
*/
export const TIER_IMPLEMENTATIONS: Record<Tier, string[]> = {
db: ['mysql'],
search: ['opensearch', 'elasticsearch'],
queue: ['rabbitmq'],
cache: ['valkey', 'redis'],
};
/**
* Flat list of every legal service name in `service_preferences`.
* Surfaced in unknown-service error messages so the caller sees
* exactly what's accepted without having to read source.
*/
export const KNOWN_SERVICE_NAMES = Object.keys(SERVICE_TIER_MAP);
/**
* Returns the tier a service name belongs to, or `undefined` if the
* name isn't recognized. Callers use the undefined return as the
* signal to reject the input as unknown.
*/
export const tierFor = (serviceName: string): Tier | undefined => {
return SERVICE_TIER_MAP[serviceName];
}
@@ -0,0 +1,68 @@
import { validatePreferencesAgainstMatrix } from './validate-preferences-against-matrix';
import { PackageMatrixVersion } from '../../matrix/matrix-type';
const baseEntry = (overrides: Partial<PackageMatrixVersion> = {}): PackageMatrixVersion => ({
magento: 'magento/project-community-edition:2.4.7',
version: '2.4.7',
php: '8.3',
composer: '2.7.4',
mysql: 'mysql:8.4',
elasticsearch: 'elasticsearch:8.11.4',
opensearch: 'opensearchproject/opensearch:2.19.1',
rabbitmq: 'rabbitmq:4.0-management',
redis: 'redis:7.2',
varnish: 'varnish:7.5',
valkey: 'valkey:8.0',
nginx: 'nginx:1.26',
os: 'ubuntu-latest',
release: '2024-04-09T00:00:00+0000',
eol: '2027-04-09T00:00:00+0000',
...overrides,
});
describe('validatePreferencesAgainstMatrix', () => {
it('does not throw when all entries support the preference', () => {
const entries = [baseEntry({ version: '2.4.7' }), baseEntry({ version: '2.4.6' })];
expect(() => validatePreferencesAgainstMatrix({ search: 'opensearch' }, entries)).not.toThrow();
});
it('throws when an entry lacks the preferenced implementation, listing the offender', () => {
const entries = [
baseEntry({ version: '2.4.7' }),
baseEntry({ version: '2.4.5', opensearch: '' }),
];
expect(() => validatePreferencesAgainstMatrix({ search: 'opensearch' }, entries)).toThrowError(
/not satisfied for:\n\s+- magento 2\.4\.5 \(supported: elasticsearch\)/
);
});
it('lists all offenders, not just the first', () => {
const entries = [
baseEntry({ version: '2.4.5', opensearch: '' }),
baseEntry({ version: '2.4.4', opensearch: '' }),
];
expect(() => validatePreferencesAgainstMatrix({ search: 'opensearch' }, entries)).toThrowError(
/magento 2\.4\.5[\s\S]*magento 2\.4\.4/
);
});
it('reports "<none>" when the entry supports nothing in the tier', () => {
const entries = [baseEntry({ version: '2.4.0', opensearch: '', elasticsearch: '' })];
expect(() => validatePreferencesAgainstMatrix({ search: 'opensearch' }, entries)).toThrowError(
/magento 2\.4\.0 \(supported: <none>\)/
);
});
it('reports violations across multiple tiers', () => {
const entries = [baseEntry({ version: '2.4.5', opensearch: '', valkey: '' })];
let captured: Error | null = null;
try {
validatePreferencesAgainstMatrix({ search: 'opensearch', cache: 'valkey' }, entries);
} catch (e) {
captured = e as Error;
}
expect(captured).not.toBeNull();
expect(captured!.message).toMatch(/opensearch/);
expect(captured!.message).toMatch(/valkey/);
});
});
@@ -0,0 +1,43 @@
import { PackageMatrixVersion } from '../../matrix/matrix-type';
import { ServicePreferences, Tier, TIER_IMPLEMENTATIONS } from './tier-map';
/**
* Verifies that every preferenced implementation is supported by every
* matrix entry. Collects all violations and throws a single
* consolidated error.
*/
export const validatePreferencesAgainstMatrix = (
preferences: ServicePreferences,
entries: PackageMatrixVersion[]
): void => {
const errors: string[] = [];
for (const tierKey of Object.keys(preferences) as Tier[]) {
const implementation = preferences[tierKey]!;
const tierImplementations = TIER_IMPLEMENTATIONS[tierKey];
const offenders: { version: string; supported: string[] }[] = [];
for (const entry of entries) {
const value = entry[implementation as keyof PackageMatrixVersion];
const isSupported = typeof value === 'string' && value.trim() !== '';
if (!isSupported) {
const supported = tierImplementations.filter(name => {
const v = entry[name as keyof PackageMatrixVersion];
return typeof v === 'string' && v.trim() !== '';
});
offenders.push({ version: entry.version, supported });
}
}
if (offenders.length > 0) {
const list = offenders
.map(o => ` - magento ${o.version} (supported: ${o.supported.length > 0 ? o.supported.join(', ') : '<none>'})`)
.join('\n');
errors.push(`service_preferences "${implementation}" is not satisfied for:\n${list}`);
}
}
if (errors.length > 0) {
throw new Error(errors.join('\n\n'));
}
}
@@ -80,4 +80,29 @@ export const valkeyConfig: ServiceTemplate = {
ports: ['6379:6379']
};
}
};
};
/**
* Builds the nginx service config for the web tier. Takes the image
* (sourced from the matrix entry's `nginx` field) and the runner
* workspace path so the volume mount lands the Magento install at
* `/var/www/html` inside the container. Pairs with `buildPhpFpmConfig`
* — they're emitted together as the web tier.
*/
export const buildNginxConfig = (image: string, workspace: string): ServiceConfig => ({
image,
ports: ['80:80'],
volumes: [`${workspace}:/var/www/html`],
options: '--health-cmd "nginx -t" --health-interval=10s --health-retries=3 --health-timeout=5s --health-start-period=5s'
});
/**
* Builds the php-fpm service config for the web tier. Composes the
* image from the matrix entry's `php` version (the mappia magento-php
* image stream is the only widely-used Magento-aware php-fpm image).
* Pairs with `buildNginxConfig`.
*/
export const buildPhpFpmConfig = (phpVersion: string, workspace: string): ServiceConfig => ({
image: `mappia/magento-php:fpm-alpine${phpVersion}`,
volumes: [`${workspace}:/var/www/html`]
});