renovatebot-github/src/renovate.ts
2026-02-02 15:28:23 +11:00

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);
}
}
}