renovatebot-github/src/renovate.ts
Jamie Tanna fde0305037
feat: show Renovate CLI version more prominently in logs (#983)
As noted in #969, it would be useful to have a more prominent output in
the logs to indicate the version of the Renovate CLI being used.

This would help with both personal debugging (for administrators of the
GitHub Action) and for raising issues upstream.

To do this, we can call the `--version` on the CLI, capture the output
and report it back to the user.

By using a Notice annotation, we can then make it more visible at the
job- and step-level.

We can then also wrap it in a `group`, so it's hidden in its own
expandable group (with timing information).

Closes #969.
2025-12-15 11:21:29 +00:00

139 lines
4.2 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');
}
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`,
);
}
}
}