mirror of
https://github.com/renovatebot/github-action.git
synced 2026-02-03 18:58:09 +00:00
165 lines
5 KiB
TypeScript
165 lines
5 KiB
TypeScript
import { exec, getExecOutput } from '@actions/exec';
|
|
import { Docker } from './docker';
|
|
import { Input } from './input';
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
export class Renovate {
|
|
static dockerGroupRegex = /^docker:x:(?<groupId>[1-9][0-9]*):/m;
|
|
private configFileMountDir = '/github-action';
|
|
|
|
private docker: Docker;
|
|
|
|
constructor(private input: Input) {
|
|
this.docker = new Docker(input);
|
|
}
|
|
|
|
async runDockerContainerForVersion(): Promise<string> {
|
|
const command = `docker run -t --rm ${this.docker.image()} --version`;
|
|
|
|
const { exitCode, stdout } = await getExecOutput(command);
|
|
if (exitCode !== 0) {
|
|
new Error(`'docker run' failed with exit code ${exitCode}.`);
|
|
}
|
|
|
|
return stdout.trim();
|
|
}
|
|
|
|
async runDockerContainer(): Promise<void> {
|
|
await this.validateArguments();
|
|
|
|
const dockerArguments = this.input
|
|
.toEnvironmentVariables()
|
|
.map((e) => `--env ${e.key}`)
|
|
.concat([`--env ${this.input.token.key}=${this.input.token.value}`]);
|
|
|
|
const configurationFile = this.input.configurationFile();
|
|
if (configurationFile !== null) {
|
|
const baseName = path.basename(configurationFile.value);
|
|
const mountPath = path.join(this.configFileMountDir, baseName);
|
|
dockerArguments.push(
|
|
`--env ${configurationFile.key}=${mountPath}`,
|
|
`--volume ${configurationFile.value}:${mountPath}`,
|
|
);
|
|
}
|
|
|
|
if (this.input.mountDockerSocket()) {
|
|
const sockPath = this.input.dockerSocketHostPath();
|
|
const stat = await fs.stat(sockPath);
|
|
if (!stat.isSocket()) {
|
|
throw new Error(
|
|
`docker socket host path '${sockPath}' MUST exist and be a socket`,
|
|
);
|
|
}
|
|
|
|
dockerArguments.push(
|
|
`--volume ${sockPath}:/var/run/docker.sock`,
|
|
`--group-add ${await this.getDockerGroupId()}`,
|
|
);
|
|
}
|
|
|
|
const dockerCmdFile = this.input.getDockerCmdFile();
|
|
let dockerCmd: string | null = null;
|
|
if (dockerCmdFile !== null) {
|
|
const baseName = path.basename(dockerCmdFile);
|
|
const mountPath = `/${baseName}`;
|
|
dockerArguments.push(`--volume ${dockerCmdFile}:${mountPath}`);
|
|
dockerCmd = mountPath;
|
|
}
|
|
|
|
const dockerUser = this.input.getDockerUser();
|
|
if (dockerUser !== null) {
|
|
dockerArguments.push(`--user ${dockerUser}`);
|
|
}
|
|
|
|
for (const volumeMount of this.input.getDockerVolumeMounts()) {
|
|
dockerArguments.push(`--volume ${volumeMount}`);
|
|
}
|
|
|
|
const dockerNetwork = this.input.getDockerNetwork();
|
|
if (dockerNetwork) {
|
|
dockerArguments.push(`--network ${dockerNetwork}`);
|
|
}
|
|
|
|
dockerArguments.push('--rm', this.docker.image());
|
|
|
|
if (dockerCmd !== null) {
|
|
dockerArguments.push(dockerCmd);
|
|
}
|
|
|
|
const command = `docker run -t ${dockerArguments.join(' ')}`;
|
|
|
|
const code = await exec(command);
|
|
if (code !== 0) {
|
|
new Error(`'docker run' failed with exit code ${code}.`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch the host docker group of the GitHub Action runner.
|
|
*
|
|
* The Renovate container needs access to this group in order to have the
|
|
* required permissions on the Docker socket.
|
|
*/
|
|
private async getDockerGroupId(): Promise<string> {
|
|
const groupFile = '/etc/group';
|
|
const groups = await fs.readFile(groupFile, {
|
|
encoding: 'utf-8',
|
|
});
|
|
|
|
/**
|
|
* The group file has `groupname:group-password:GID:username-list` as
|
|
* structure and we're interested in the `GID` (the group ID).
|
|
*
|
|
* Source: https://www.thegeekdiary.com/etcgroup-file-explained/
|
|
*/
|
|
const match = Renovate.dockerGroupRegex.exec(groups);
|
|
if (match?.groups?.groupId === undefined) {
|
|
throw new Error(`Could not find group docker in ${groupFile}`);
|
|
}
|
|
|
|
return match.groups.groupId;
|
|
}
|
|
|
|
private async validateArguments(): Promise<void> {
|
|
if (/\s/.test(this.input.token.value)) {
|
|
throw new Error('Token MUST NOT contain whitespace');
|
|
}
|
|
await this.validateConfigFileArgument();
|
|
await this.validateDockerCmdFileArgument();
|
|
}
|
|
|
|
private async validateConfigFileArgument(): Promise<void> {
|
|
const configurationFile = this.input.configurationFile();
|
|
if (
|
|
configurationFile !== null &&
|
|
!(await fs.stat(configurationFile.value)).isFile()
|
|
) {
|
|
throw new Error(
|
|
`configuration file '${configurationFile.value}' MUST be an existing file`,
|
|
);
|
|
}
|
|
}
|
|
|
|
private async validateDockerCmdFileArgument(): Promise<void> {
|
|
const dockerCmdFile = this.input.getDockerCmdFile();
|
|
if (dockerCmdFile === null) return;
|
|
|
|
try {
|
|
const s = await fs.stat(dockerCmdFile);
|
|
if (!s.isFile)
|
|
throw new Error(`dockerCmdFile '${dockerCmdFile}' MUST be a file`);
|
|
if (
|
|
(s.mode & fs.constants.R_OK) === 0 ||
|
|
(s.mode & fs.constants.X_OK) === 0
|
|
)
|
|
throw new Error(
|
|
`dockerCmdFile '${dockerCmdFile}' MUST have read and execute rights`,
|
|
);
|
|
} catch (err) {
|
|
if (err instanceof Error && 'code' in err && err.code === 'ENOENT')
|
|
throw new Error(`dockerCmdFile '${dockerCmdFile}' does not exist`);
|
|
throw new Error(err as string);
|
|
}
|
|
}
|
|
}
|