mirror of
https://github.com/graycoreio/github-actions-magento2.git
synced 2026-06-08 19:46:41 +00:00
feat(setup-install): add a container_id input to run setup:install against a specific container (#255)
This commit is contained in:
@@ -18,6 +18,11 @@ inputs:
|
||||
default: ""
|
||||
description: "Additional raw flags to append to the setup:install command."
|
||||
|
||||
container_id:
|
||||
required: false
|
||||
default: ""
|
||||
description: "When set, runs setup:install via `docker exec` inside the container with this `container_id` (typically the value of `job.services['php-fpm'].id`) and uses service-network aliases (mysql, redis, etc.) instead of runner-side localhost when running setup:install."
|
||||
|
||||
outputs:
|
||||
command:
|
||||
description: "The full bin/magento setup:install command that was run."
|
||||
|
||||
Vendored
+28
-28
File diff suppressed because one or more lines are too long
@@ -173,4 +173,100 @@ describe('buildInstallArgs', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('container mode', () => {
|
||||
it('uses the mysql network alias and the internal port from ports[0]', () => {
|
||||
const services: Services = {
|
||||
mysql: { ...MYSQL_SERVICE, ports: ['33060:3306'] },
|
||||
};
|
||||
expect(buildInstallArgs(services, true)).toEqual([
|
||||
...BASE_ARGS,
|
||||
'--db-host=mysql:3306',
|
||||
'--db-name=magento_integration_tests',
|
||||
'--db-user=user',
|
||||
'--db-password=password',
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the opensearch alias and parses the internal port', () => {
|
||||
const services: Services = {
|
||||
opensearch: { image: 'opensearchproject/opensearch:2.19.1', ports: ['19200:9200'] },
|
||||
};
|
||||
expect(buildInstallArgs(services, true)).toEqual([
|
||||
...BASE_ARGS,
|
||||
'--search-engine=opensearch',
|
||||
'--opensearch-host=opensearch',
|
||||
'--opensearch-port=9200',
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the elasticsearch alias and parses the internal port', () => {
|
||||
const services: Services = {
|
||||
elasticsearch: { image: 'elasticsearch:8.11.4', ports: ['19200:9200'] },
|
||||
};
|
||||
expect(buildInstallArgs(services, true)).toEqual([
|
||||
...BASE_ARGS,
|
||||
'--search-engine=elasticsearch8',
|
||||
'--elasticsearch-host=elasticsearch',
|
||||
'--elasticsearch-port=9200',
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the rabbitmq alias and parses the internal port', () => {
|
||||
const services: Services = {
|
||||
rabbitmq: { image: 'rabbitmq:4.0-management', ports: ['15672:5672'] },
|
||||
};
|
||||
expect(buildInstallArgs(services, true)).toEqual([
|
||||
...BASE_ARGS,
|
||||
'--amqp-host=rabbitmq',
|
||||
'--amqp-port=5672',
|
||||
'--amqp-user=guest',
|
||||
'--amqp-password=guest',
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the valkey alias when valkey is the cache service', () => {
|
||||
const services: Services = {
|
||||
valkey: { image: 'valkey:8.0', ports: ['16379:6379'] },
|
||||
};
|
||||
expect(buildInstallArgs(services, true)).toEqual([
|
||||
...BASE_ARGS,
|
||||
'--session-save=redis',
|
||||
'--session-save-redis-host=valkey',
|
||||
'--session-save-redis-port=6379',
|
||||
'--cache-backend=redis',
|
||||
'--cache-backend-redis-server=valkey',
|
||||
'--cache-backend-redis-port=6379',
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the redis alias when redis is the cache service', () => {
|
||||
const services: Services = {
|
||||
redis: { image: 'redis:7.2', ports: ['16379:6379'] },
|
||||
};
|
||||
expect(buildInstallArgs(services, true)).toEqual([
|
||||
...BASE_ARGS,
|
||||
'--session-save=redis',
|
||||
'--session-save-redis-host=redis',
|
||||
'--session-save-redis-port=6379',
|
||||
'--cache-backend=redis',
|
||||
'--cache-backend-redis-server=redis',
|
||||
'--cache-backend-redis-port=6379',
|
||||
]);
|
||||
});
|
||||
|
||||
it('falls back to default internal ports when ports are absent', () => {
|
||||
const services: Services = {
|
||||
mysql: { ...MYSQL_SERVICE, ports: undefined },
|
||||
opensearch: { image: 'opensearchproject/opensearch:2.19.1' },
|
||||
rabbitmq: { image: 'rabbitmq:4.0-management' },
|
||||
valkey: { image: 'valkey:8.0' },
|
||||
};
|
||||
const args = buildInstallArgs(services, true);
|
||||
expect(args).toContain('--db-host=mysql:3306');
|
||||
expect(args).toContain('--opensearch-port=9200');
|
||||
expect(args).toContain('--amqp-port=5672');
|
||||
expect(args).toContain('--session-save-redis-port=6379');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ export interface ServiceConfig {
|
||||
env?: Record<string, string>;
|
||||
ports?: string[];
|
||||
options?: string;
|
||||
volumes?: string[];
|
||||
}
|
||||
|
||||
export interface Services {
|
||||
@@ -12,6 +13,7 @@ export interface Services {
|
||||
rabbitmq?: ServiceConfig;
|
||||
redis?: ServiceConfig;
|
||||
valkey?: ServiceConfig;
|
||||
'php-fpm'?: ServiceConfig;
|
||||
}
|
||||
|
||||
const BASE_ARGS = [
|
||||
@@ -24,21 +26,29 @@ const BASE_ARGS = [
|
||||
'--backend-frontname=admin',
|
||||
];
|
||||
|
||||
const parsePort = (svc: ServiceConfig | undefined, index: 0 | 1, fallback: string): string => {
|
||||
return svc?.ports?.[0]?.split(':')[index] ?? fallback;
|
||||
};
|
||||
|
||||
export const buildMysqlPrepArgs = (mysql: ServiceConfig): string[] => {
|
||||
const rootPassword = mysql.env?.MYSQL_ROOT_PASSWORD ?? 'rootpassword';
|
||||
const port = mysql.ports?.[0]?.split(':')[0] ?? '3306';
|
||||
const port = parsePort(mysql, 0, '3306');
|
||||
return ['-h127.0.0.1', `--port=${port}`, '-uroot', `-p${rootPassword}`, '-e', 'SET GLOBAL log_bin_trust_function_creators = 1;'];
|
||||
};
|
||||
|
||||
export const buildInstallArgs = (services: Services | null): string[] => {
|
||||
export const buildInstallArgs = (services: Services | null, containerMode = false): string[] => {
|
||||
const args = [...BASE_ARGS];
|
||||
|
||||
if (!services) return args;
|
||||
|
||||
const portIdx: 0 | 1 = containerMode ? 1 : 0;
|
||||
const hostFor = (alias: string): string => containerMode ? alias : 'localhost';
|
||||
|
||||
if (services.mysql) {
|
||||
const dbPort = services.mysql.ports?.[0]?.split(':')[0] ?? '3306';
|
||||
const dbPort = parsePort(services.mysql, portIdx, '3306');
|
||||
const dbHost = containerMode ? `mysql:${dbPort}` : `127.0.0.1:${dbPort}`;
|
||||
args.push(
|
||||
`--db-host=127.0.0.1:${dbPort}`,
|
||||
`--db-host=${dbHost}`,
|
||||
`--db-name=${services.mysql.env?.MYSQL_DATABASE ?? 'magento'}`,
|
||||
`--db-user=${services.mysql.env?.MYSQL_USER ?? 'magento'}`,
|
||||
`--db-password=${services.mysql.env?.MYSQL_PASSWORD ?? 'magento'}`,
|
||||
@@ -46,37 +56,44 @@ export const buildInstallArgs = (services: Services | null): string[] => {
|
||||
}
|
||||
|
||||
if (services.opensearch) {
|
||||
const port = parsePort(services.opensearch, portIdx, '9200');
|
||||
args.push(
|
||||
'--search-engine=opensearch',
|
||||
'--opensearch-host=localhost',
|
||||
'--opensearch-port=9200',
|
||||
`--opensearch-host=${hostFor('opensearch')}`,
|
||||
`--opensearch-port=${port}`,
|
||||
);
|
||||
} else if (services.elasticsearch) {
|
||||
const majorVersion = services.elasticsearch.image.split(':')[1]?.split('.')[0];
|
||||
const port = parsePort(services.elasticsearch, portIdx, '9200');
|
||||
args.push(
|
||||
`--search-engine=elasticsearch${majorVersion}`,
|
||||
'--elasticsearch-host=localhost',
|
||||
'--elasticsearch-port=9200',
|
||||
`--elasticsearch-host=${hostFor('elasticsearch')}`,
|
||||
`--elasticsearch-port=${port}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (services.rabbitmq) {
|
||||
const port = parsePort(services.rabbitmq, portIdx, '5672');
|
||||
args.push(
|
||||
'--amqp-host=localhost',
|
||||
'--amqp-port=5672',
|
||||
`--amqp-host=${hostFor('rabbitmq')}`,
|
||||
`--amqp-port=${port}`,
|
||||
'--amqp-user=guest',
|
||||
'--amqp-password=guest',
|
||||
);
|
||||
}
|
||||
|
||||
if (services.valkey || services.redis) {
|
||||
const cacheKey: 'valkey' | 'redis' = services.valkey ? 'valkey' : 'redis';
|
||||
const cache = services[cacheKey]!;
|
||||
const port = parsePort(cache, portIdx, '6379');
|
||||
const host = hostFor(cacheKey);
|
||||
args.push(
|
||||
'--session-save=redis',
|
||||
'--session-save-redis-host=localhost',
|
||||
'--session-save-redis-port=6379',
|
||||
`--session-save-redis-host=${host}`,
|
||||
`--session-save-redis-port=${port}`,
|
||||
'--cache-backend=redis',
|
||||
'--cache-backend-redis-server=localhost',
|
||||
'--cache-backend-redis-port=6379',
|
||||
`--cache-backend-redis-server=${host}`,
|
||||
`--cache-backend-redis-port=${port}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as exec from '@actions/exec';
|
||||
import * as nodePath from 'path';
|
||||
import { buildInstallArgs, buildMysqlPrepArgs, Services } from './build-command';
|
||||
|
||||
const resolveContainerPath = (runnerPath: string): string => {
|
||||
const workspace = process.env.GITHUB_WORKSPACE || '';
|
||||
const absolute = nodePath.resolve(workspace, runnerPath);
|
||||
const relative = nodePath.relative(workspace, absolute);
|
||||
if (relative.startsWith('..')) {
|
||||
throw new Error(`container_id: path ${runnerPath} resolves outside GITHUB_WORKSPACE (${workspace})`);
|
||||
}
|
||||
return relative ? `/var/www/html/${relative}` : '/var/www/html';
|
||||
};
|
||||
|
||||
export async function run(): Promise<void> {
|
||||
try {
|
||||
const servicesInput = core.getInput('services');
|
||||
const path = core.getInput('path') || '.';
|
||||
const extraArgs = core.getInput('extra_args').trim();
|
||||
const containerId = core.getInput('container_id').trim();
|
||||
const containerMode = containerId !== '';
|
||||
|
||||
let services: Services | null = null;
|
||||
if (servicesInput && servicesInput !== 'null') {
|
||||
@@ -14,20 +27,42 @@ export async function run(): Promise<void> {
|
||||
}
|
||||
|
||||
// setup:install creates MySQL triggers, which requires log_bin_trust_function_creators=1
|
||||
// when binary logging is enabled.
|
||||
// when binary logging is enabled. The prep always runs runner-side against the published port.
|
||||
if (services?.mysql) {
|
||||
await exec.exec('mysql', buildMysqlPrepArgs(services.mysql));
|
||||
}
|
||||
|
||||
const args = buildInstallArgs(services);
|
||||
const args = buildInstallArgs(services, containerMode);
|
||||
|
||||
if (extraArgs) {
|
||||
args.push(...extraArgs.split(/\s+/));
|
||||
}
|
||||
|
||||
core.setOutput('command', `php bin/magento setup:install ${args.join(' ')}`);
|
||||
if (containerMode) {
|
||||
const containerPath = resolveContainerPath(path);
|
||||
|
||||
await exec.exec('php', ['bin/magento', 'setup:install', ...args], { cwd: path });
|
||||
const command = `docker exec -w ${containerPath} ${containerId} php bin/magento setup:install ${args.join(' ')}`;
|
||||
core.setOutput('command', command);
|
||||
|
||||
await exec.exec('docker', [
|
||||
'exec',
|
||||
'-w', containerPath,
|
||||
containerId,
|
||||
'php', 'bin/magento', 'setup:install',
|
||||
...args,
|
||||
]);
|
||||
|
||||
// setup:install runs as root inside the container, but php-fpm workers
|
||||
// serve requests as `www-data`. Hand ownership of the Magento writable
|
||||
// dirs to www-data so request-time cache/log writes succeed.
|
||||
await exec.exec('docker', [
|
||||
'exec', containerId, 'sh', '-c',
|
||||
`for d in var generated pub/static pub/media; do [ -d "${containerPath}/$d" ] && chown -R www-data:www-data "${containerPath}/$d"; done`,
|
||||
]);
|
||||
} else {
|
||||
core.setOutput('command', `php bin/magento setup:install ${args.join(' ')}`);
|
||||
await exec.exec('php', ['bin/magento', 'setup:install', ...args], { cwd: path });
|
||||
}
|
||||
} catch (error) {
|
||||
core.setFailed((error as Error).message);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user