Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: local runner #86

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"author": "Rhys Arkins <[email protected]>",
"main": "dist/index.js",
"engines": {
"node": " >=12.0.0",
"node": ">=12.0.0",
"yarn": ">=1.22.0"
},
"scripts": {
Expand All @@ -26,17 +26,21 @@
"@actions/io": "1.0.2",
"@sindresorhus/is": "3.1.2",
"chalk": "4.1.0",
"commander": "5.1.0",
"delay": "4.4.0",
"find-up": "5.0.0",
"got": "11.7.0",
"js-yaml": "3.14.0",
"renovate": "23.39.0",
"semver": "7.3.2",
"strip-ansi": "6.0.0",
"www-authenticate": "0.6.2"
"www-authenticate": "0.6.2",
"yawn-yaml": "1.5.0"
},
"devDependencies": {
"@jest/globals": "26.4.2",
"@types/jest": "26.0.14",
"@types/js-yaml": "3.12.4",
"@types/node": "12.12.62",
"@types/semver": "7.3.4",
"@typescript-eslint/eslint-plugin": "4.3.0",
Expand Down
13 changes: 7 additions & 6 deletions src/commands/docker/builder.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { getInput } from '@actions/core';
import is from '@sindresorhus/is';
import chalk from 'chalk';
import { ReleaseResult, getPkgReleases } from 'renovate/dist/datasource';
import { get as getVersioning } from 'renovate/dist/versioning';
import { exec, getArg, isDryRun, readFile, readJson } from '../../util';
import { exec, isDryRun } from '../../util';
import { getArg } from '../../utils/cli';
import { build, publish } from '../../utils/docker';
import { init } from '../../utils/docker/buildx';
import { dockerDf, dockerPrune, dockerTag } from '../../utils/docker/common';
import { readFile, readJson } from '../../utils/fs';
import log from '../../utils/logger';
import * as renovate from '../../utils/renovate';

Expand Down Expand Up @@ -291,7 +292,7 @@ async function readDockerConfig(cfg: ConfigFile): Promise<void> {

export async function run(): Promise<void> {
const dryRun = isDryRun();
const configFile = getInput('config') || 'builder.json';
const configFile = getArg('config') || 'builder.json';

const cfg = await readJson<ConfigFile>(configFile);

Expand All @@ -301,7 +302,7 @@ export async function run(): Promise<void> {

// TODO: validation
if (!is.string(cfg.image)) {
cfg.image = getInput('image', { required: true });
cfg.image = getArg('image', { required: true });
}

if (!is.string(cfg.buildArg)) {
Expand All @@ -319,8 +320,8 @@ export async function run(): Promise<void> {
tagSuffix: getArg('tag-suffix') || undefined,
ignoredVersions: cfg.ignoredVersions ?? [],
dryRun,
lastOnly: getInput('last-only') == 'true',
buildOnly: getInput('build-only') == 'true',
lastOnly: getArg('last-only') == 'true',
buildOnly: getArg('build-only') == 'true',
majorMinor: getArg('major-minor') !== 'false',
prune: getArg('prune') === 'true',
};
Expand Down
9 changes: 7 additions & 2 deletions src/runner.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { getInput, setFailed } from '@actions/core';
import { setFailed } from '@actions/core';
import chalk from 'chalk';
import { Commands } from './types';
import { getArg, initCli, isCli } from './utils/cli';
import log from './utils/logger';

export default async function run(): Promise<void> {
try {
log.info(chalk.blue('Renovate Docker Builder'));
const cmd = getInput('command') as Commands;
if (isCli()) {
await initCli();
}
const cmd = getArg('command') as Commands;
log.info(chalk.yellow('Executing:'), ` ${cmd}`);
switch (cmd) {
case Commands.DockerBuilder:
Expand All @@ -18,6 +22,7 @@ export default async function run(): Promise<void> {
break;

default:
log(chalk.yellow('args:'), chalk.grey(...process.argv));
log.error(chalk.red('Unknown command:'), cmd);
break;
}
Expand Down
63 changes: 1 addition & 62 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { promises as fs } from 'fs';
import { join } from 'path';
import { endGroup, getInput, startGroup } from '@actions/core';
import { exec as _exec } from '@actions/exec';
import { ExecOptions as _ExecOptions } from '@actions/exec/lib/interfaces';
import findUp = require('find-up');
import { getEnv } from './utils/env';
import { ExecError, ExecResult } from './utils/types';

export type ExecOptions = _ExecOptions;
Expand Down Expand Up @@ -41,15 +39,6 @@ export async function exec(
return { code, stdout, stderr };
}

/**
* Get environment variable or empty string.
* Used for easy mocking.
* @param key variable name
*/
export function getEnv(key: string): string {
return process.env[key] ?? '';
}

export function isCI(): boolean {
return !!getEnv('CI');
}
Expand All @@ -62,53 +51,3 @@ export function isDryRun(): boolean {
export function getWorkspace(): string {
return getEnv('GITHUB_WORKSPACE') || process.cwd();
}

export async function readJson<T = unknown>(file: string): Promise<T> {
const path = join(getWorkspace(), file);
const res = (await import(path)) as T | { default: T };
// istanbul ignore next
return 'default' in res ? res?.default : res;
}

export async function readFile(file: string): Promise<string> {
const path = join(getWorkspace(), file);
return await fs.readFile(path, 'utf8');
}

export const MultiArgsSplitRe = /\s*(?:[;,]|$)\s*/;

export function getArg(name: string, opts?: { required?: boolean }): string;
export function getArg(
name: string,
opts?: { required?: boolean; multi: true }
): string[];
export function getArg(
name: string,
opts?: { required?: boolean; multi?: boolean }
): string | string[];
export function getArg(
name: string,
opts?: { required?: boolean; multi?: boolean }
): string | string[] {
const val = getInput(name, opts);
return opts?.multi ? val.split(MultiArgsSplitRe).filter(Boolean) : val;
}

let _pkg: Promise<string | undefined>;

/**
* Resolve path for a file relative to renovate root directory (our package.json)
* @param file a file to resolve
*/
export async function resolveFile(file: string): Promise<string> {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
if (!_pkg) {
_pkg = findUp('package.json', { cwd: __dirname, type: 'file' });
}
const pkg = await _pkg;
// istanbul ignore if
if (!pkg) {
throw new Error('Missing package.json');
}
return join(pkg, '../', file);
}
74 changes: 74 additions & 0 deletions src/utils/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { getInput as cli } from '@actions/core';
import { Command } from 'commander';
import { getEnv } from './env';
import { readLocalYaml } from './fs';

export function isCli(): boolean {
return getEnv('GITHUB_ACTIONS') !== 'true';
}

export function getArgv(): string[] {
return process.argv;
}

type Action = {
inputs: Record<
string,
{ description: string; required: boolean; default: string }
>;
};

let _opts: Record<string, string> = {};

async function parse(): Promise<Record<string, string>> {
const program = new Command()
.storeOptionsAsProperties(false)
.passCommandToAction(false)
.arguments('<command> [image]');

const action = await readLocalYaml<Action>('action.yml');

for (const name of Object.keys(action.inputs).filter(
(n) => !['command', 'image'].includes(n)
)) {
const val = action.inputs[name];

if (val.required === true) {
program.requiredOption(`--${name} <value>`, val.description, val.default);
} else {
program.option(`--${name} <value>`, val.description, val.default);
}
}

return program.parse(getArgv()).opts();
}

export async function initCli(): Promise<void> {
_opts = await parse();
}

export function getCliArg(name: string, required?: boolean): string {
if (!_opts[name] && required) {
throw Error(`Missing required argument ${name}`);
}
return _opts[name];
}

export const MultiArgsSplitRe = /\s*(?:[;,]|$)\s*/;

export function getArg(name: string, opts?: { required?: boolean }): string;
export function getArg(
name: string,
opts?: { required?: boolean; multi: true }
): string[];
export function getArg(
name: string,
opts?: { required?: boolean; multi?: boolean }
): string | string[];
export function getArg(
name: string,
opts?: { required?: boolean; multi?: boolean }
): string | string[] {
const val = isCli() ? getCliArg(name, opts?.required) : cli(name, opts);
return opts?.multi ? val.split(MultiArgsSplitRe).filter(Boolean) : val;
}
10 changes: 9 additions & 1 deletion src/utils/docker/buildx.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { existsSync } from 'fs';
import { platform } from 'os';
import { join } from 'path';
import { exec, resolveFile } from '../../util';
import { exec } from '../../util';
import { resolveFile } from '../fs';
import log from '../logger';
import { docker, dockerBuildx, dockerRun } from './common';

Expand All @@ -10,6 +12,12 @@ export async function init(): Promise<void> {
`.docker/cli-plugins/docker-buildx`
);

// istanbul ignore if
if (platform() !== 'linux') {
log.warn('Buildx support only on linux');
return;
}

// istanbul ignore if
if (existsSync(buildx)) {
log('Buildx already initialized');
Expand Down
8 changes: 8 additions & 0 deletions src/utils/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Get environment variable or empty string.
* Used for easy mocking.
* @param key variable name
*/
export function getEnv(key: string): string {
return process.env[key] ?? '';
}
41 changes: 41 additions & 0 deletions src/utils/fs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import findUp = require('find-up');
import { promises as fs } from 'fs';
import { safeLoad } from 'js-yaml';
import { join } from 'upath';

let _pkg: Promise<string | undefined>;

/**
* Resolve path for a file relative to renovate root directory (our package.json)
* @param file a file to resolve
*/
export async function resolveFile(file: string): Promise<string> {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
if (!_pkg) {
_pkg = findUp('package.json', { cwd: __dirname, type: 'file' });
}
const pkg = await _pkg;
// istanbul ignore if
if (!pkg) {
throw new Error('Missing package.json');
}
return join(pkg, '../', file);
}

export async function readJson<T = unknown>(file: string): Promise<T> {
const path = join(process.cwd(), file);
const res = (await import(path)) as T | { default: T };
// istanbul ignore next
return 'default' in res ? res?.default : res;
}

export async function readFile(file: string): Promise<string> {
const path = join(process.cwd(), file);
return await fs.readFile(path, 'utf8');
}

export async function readLocalYaml<T = unknown>(file: string): Promise<T> {
const path = await resolveFile(file);
const res = await fs.readFile(path, 'utf-8');
return safeLoad(res, { json: true }) as T;
}
Loading