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: ""
|
default: ""
|
||||||
description: "Additional raw flags to append to the setup:install command."
|
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:
|
outputs:
|
||||||
command:
|
command:
|
||||||
description: "The full bin/magento setup:install command that was run."
|
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>;
|
env?: Record<string, string>;
|
||||||
ports?: string[];
|
ports?: string[];
|
||||||
options?: string;
|
options?: string;
|
||||||
|
volumes?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Services {
|
export interface Services {
|
||||||
@@ -12,6 +13,7 @@ export interface Services {
|
|||||||
rabbitmq?: ServiceConfig;
|
rabbitmq?: ServiceConfig;
|
||||||
redis?: ServiceConfig;
|
redis?: ServiceConfig;
|
||||||
valkey?: ServiceConfig;
|
valkey?: ServiceConfig;
|
||||||
|
'php-fpm'?: ServiceConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BASE_ARGS = [
|
const BASE_ARGS = [
|
||||||
@@ -24,21 +26,29 @@ const BASE_ARGS = [
|
|||||||
'--backend-frontname=admin',
|
'--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[] => {
|
export const buildMysqlPrepArgs = (mysql: ServiceConfig): string[] => {
|
||||||
const rootPassword = mysql.env?.MYSQL_ROOT_PASSWORD ?? 'rootpassword';
|
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;'];
|
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];
|
const args = [...BASE_ARGS];
|
||||||
|
|
||||||
if (!services) return args;
|
if (!services) return args;
|
||||||
|
|
||||||
|
const portIdx: 0 | 1 = containerMode ? 1 : 0;
|
||||||
|
const hostFor = (alias: string): string => containerMode ? alias : 'localhost';
|
||||||
|
|
||||||
if (services.mysql) {
|
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(
|
args.push(
|
||||||
`--db-host=127.0.0.1:${dbPort}`,
|
`--db-host=${dbHost}`,
|
||||||
`--db-name=${services.mysql.env?.MYSQL_DATABASE ?? 'magento'}`,
|
`--db-name=${services.mysql.env?.MYSQL_DATABASE ?? 'magento'}`,
|
||||||
`--db-user=${services.mysql.env?.MYSQL_USER ?? 'magento'}`,
|
`--db-user=${services.mysql.env?.MYSQL_USER ?? 'magento'}`,
|
||||||
`--db-password=${services.mysql.env?.MYSQL_PASSWORD ?? 'magento'}`,
|
`--db-password=${services.mysql.env?.MYSQL_PASSWORD ?? 'magento'}`,
|
||||||
@@ -46,39 +56,46 @@ export const buildInstallArgs = (services: Services | null): string[] => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (services.opensearch) {
|
if (services.opensearch) {
|
||||||
|
const port = parsePort(services.opensearch, portIdx, '9200');
|
||||||
args.push(
|
args.push(
|
||||||
'--search-engine=opensearch',
|
'--search-engine=opensearch',
|
||||||
'--opensearch-host=localhost',
|
`--opensearch-host=${hostFor('opensearch')}`,
|
||||||
'--opensearch-port=9200',
|
`--opensearch-port=${port}`,
|
||||||
);
|
);
|
||||||
} else if (services.elasticsearch) {
|
} else if (services.elasticsearch) {
|
||||||
const majorVersion = services.elasticsearch.image.split(':')[1]?.split('.')[0];
|
const majorVersion = services.elasticsearch.image.split(':')[1]?.split('.')[0];
|
||||||
|
const port = parsePort(services.elasticsearch, portIdx, '9200');
|
||||||
args.push(
|
args.push(
|
||||||
`--search-engine=elasticsearch${majorVersion}`,
|
`--search-engine=elasticsearch${majorVersion}`,
|
||||||
'--elasticsearch-host=localhost',
|
`--elasticsearch-host=${hostFor('elasticsearch')}`,
|
||||||
'--elasticsearch-port=9200',
|
`--elasticsearch-port=${port}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (services.rabbitmq) {
|
if (services.rabbitmq) {
|
||||||
|
const port = parsePort(services.rabbitmq, portIdx, '5672');
|
||||||
args.push(
|
args.push(
|
||||||
'--amqp-host=localhost',
|
`--amqp-host=${hostFor('rabbitmq')}`,
|
||||||
'--amqp-port=5672',
|
`--amqp-port=${port}`,
|
||||||
'--amqp-user=guest',
|
'--amqp-user=guest',
|
||||||
'--amqp-password=guest',
|
'--amqp-password=guest',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (services.valkey || services.redis) {
|
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(
|
args.push(
|
||||||
'--session-save=redis',
|
'--session-save=redis',
|
||||||
'--session-save-redis-host=localhost',
|
`--session-save-redis-host=${host}`,
|
||||||
'--session-save-redis-port=6379',
|
`--session-save-redis-port=${port}`,
|
||||||
'--cache-backend=redis',
|
'--cache-backend=redis',
|
||||||
'--cache-backend-redis-server=localhost',
|
`--cache-backend-redis-server=${host}`,
|
||||||
'--cache-backend-redis-port=6379',
|
`--cache-backend-redis-port=${port}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return args;
|
return args;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,25 @@
|
|||||||
import * as core from '@actions/core';
|
import * as core from '@actions/core';
|
||||||
import * as exec from '@actions/exec';
|
import * as exec from '@actions/exec';
|
||||||
|
import * as nodePath from 'path';
|
||||||
import { buildInstallArgs, buildMysqlPrepArgs, Services } from './build-command';
|
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> {
|
export async function run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const servicesInput = core.getInput('services');
|
const servicesInput = core.getInput('services');
|
||||||
const path = core.getInput('path') || '.';
|
const path = core.getInput('path') || '.';
|
||||||
const extraArgs = core.getInput('extra_args').trim();
|
const extraArgs = core.getInput('extra_args').trim();
|
||||||
|
const containerId = core.getInput('container_id').trim();
|
||||||
|
const containerMode = containerId !== '';
|
||||||
|
|
||||||
let services: Services | null = null;
|
let services: Services | null = null;
|
||||||
if (servicesInput && servicesInput !== 'null') {
|
if (servicesInput && servicesInput !== 'null') {
|
||||||
@@ -14,23 +27,45 @@ export async function run(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// setup:install creates MySQL triggers, which requires log_bin_trust_function_creators=1
|
// 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) {
|
if (services?.mysql) {
|
||||||
await exec.exec('mysql', buildMysqlPrepArgs(services.mysql));
|
await exec.exec('mysql', buildMysqlPrepArgs(services.mysql));
|
||||||
}
|
}
|
||||||
|
|
||||||
const args = buildInstallArgs(services);
|
const args = buildInstallArgs(services, containerMode);
|
||||||
|
|
||||||
if (extraArgs) {
|
if (extraArgs) {
|
||||||
args.push(...extraArgs.split(/\s+/));
|
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) {
|
} catch (error) {
|
||||||
core.setFailed((error as Error).message);
|
core.setFailed((error as Error).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
run();
|
run();
|
||||||
|
|||||||
Reference in New Issue
Block a user