Skip to content

Commit

Permalink
Autofix Renovate presets (#1117)
Browse files Browse the repository at this point in the history
  • Loading branch information
72636c authored Mar 18, 2023
1 parent 84a5838 commit dbd4853
Show file tree
Hide file tree
Showing 12 changed files with 398 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/afraid-melons-beam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'skuba': patch
---

init: Include baseline SEEK `renovate-config` preset
25 changes: 25 additions & 0 deletions .changeset/breezy-goats-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'skuba': minor
---

format, lint: Prepend baseline SEEK `renovate-config` preset

`skuba format` and `skuba lint` will now automatically prepend an appropriate baseline preset if your project is configured with a `SEEK-Jobs` or `seekasia` remote:

```diff
// SEEK-Jobs
{
- extends: ['seek'],
+ extends: ['local>seek-jobs/renovate-config', 'seek'],
}

// seekasia
{
- extends: ['seek'],
+ extends: ['local>seekasia/renovate-config', 'seek'],
}
```

Renovate requires this new configuration to reliably access private SEEK packages. Adding the preset should fix recent issues where Renovate would open then autoclose pull requests, and report ⚠ Dependency Lookup Warnings ⚠.

See [SEEK-Jobs/renovate-config](https://github.com/SEEK-Jobs/renovate-config) and [seekasia/renovate-config](https://github.com/seekasia/renovate-config) for more information.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"fs-extra": "^11.0.0",
"function-arguments": "^1.0.9",
"get-port": "^5.1.1",
"golden-fleece": "^1.0.9",
"ignore": "^5.1.8",
"is-installed-globally": "^0.4.0",
"isomorphic-git": "^1.11.1",
Expand Down
5 changes: 5 additions & 0 deletions src/cli/configure/modules/renovate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ const OTHER_CONFIG_FILENAMES = [
'renovate.json5',
];

export const RENOVATE_CONFIG_FILENAMES = [
'.github/renovate.json5',
...OTHER_CONFIG_FILENAMES,
];

export const renovateModule = async ({ type }: Options): Promise<Module> => {
const configFile = await readBaseTemplateFile('.github/renovate.json5');

Expand Down
185 changes: 185 additions & 0 deletions src/cli/configure/patchRenovateConfig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { inspect } from 'util';

import memfs, { vol } from 'memfs';

import * as Git from '../../api/git';

import { tryPatchRenovateConfig } from './patchRenovateConfig';

jest.mock('fs-extra', () => memfs);

const JSON = `
{
"extends": [
"github>seek-oss/rynovate:third-party-major"
]
}
`;

const JSON5 = `
{
extends: [
// Preceding comment
'seek',
// Succeeding comment
]
}
`;

const JSON5_CONFIGURED = `{extends: ['github>seek-oss/rynovate', local>seek-jobs/renovate-config']}`;

const getOwnerAndRepo = jest.spyOn(Git, 'getOwnerAndRepo');

const consoleLog = jest.spyOn(console, 'log').mockImplementation();

const writeFile = jest.spyOn(memfs.fs.promises, 'writeFile');

const volToJson = () => vol.toJSON(process.cwd(), undefined, true);

beforeEach(jest.clearAllMocks);
beforeEach(() => vol.reset());

it('patches a JSON config for a SEEK-Jobs project', async () => {
getOwnerAndRepo.mockResolvedValue({ owner: 'SEEK-Jobs', repo: 'VersionNet' });

vol.fromJSON({ 'renovate.json': JSON });

await expect(tryPatchRenovateConfig()).resolves.toBeUndefined();

expect(volToJson()).toMatchInlineSnapshot(`
{
"renovate.json": "{
"extends": [
"local>seek-jobs/renovate-config",
"github>seek-oss/rynovate:third-party-major"
]
}
",
}
`);
});

it('patches a JSON5 config for a seekasia project', async () => {
getOwnerAndRepo.mockResolvedValue({
owner: 'sEEkAsIa',
repo: 'VersionCobol',
});

vol.fromJSON({ '.github/renovate.json5': JSON5 });

await expect(tryPatchRenovateConfig()).resolves.toBeUndefined();

// Note that `golden-fleece` can't do any better than this imperfect output,
// but at least it allows us to preserve the comments rather than dropping
// them entirely.
expect(volToJson()).toMatchInlineSnapshot(`
{
".github/renovate.json5": "{
extends: [
// Preceding comment
'local>seekasia/renovate-config',
'seek',
// Succeeding comment
],
}
",
}
`);
});

it('handles a lack of Renovate config', async () => {
getOwnerAndRepo.mockResolvedValue({ owner: 'SEEK-Jobs', repo: 'monolith' });

await expect(tryPatchRenovateConfig()).resolves.toBeUndefined();

expect(volToJson()).toStrictEqual({});
});

it('handles a filesystem error', async () => {
const err = new Error('Badness!');

writeFile.mockRejectedValueOnce(err);

getOwnerAndRepo.mockResolvedValue({ owner: 'SEEK-Jobs', repo: 'VersionNet' });

const files = { 'renovate.json5': JSON5 };

vol.fromJSON(files);

await expect(tryPatchRenovateConfig()).resolves.toBeUndefined();

expect(volToJson()).toStrictEqual(files);

expect(consoleLog).toHaveBeenCalledWith('Failed to patch Renovate config.');
expect(consoleLog).toHaveBeenCalledWith(inspect(err));
});

it('handles a non-Git directory', async () => {
const err = new Error('Badness!');

getOwnerAndRepo.mockRejectedValue(err);

await expect(tryPatchRenovateConfig()).resolves.toBeUndefined();

expect(volToJson()).toStrictEqual({});

expect(consoleLog).toHaveBeenCalledWith('Failed to patch Renovate config.');
expect(consoleLog).toHaveBeenCalledWith(inspect(err));
});

it('skips a seek-oss project', async () => {
getOwnerAndRepo.mockResolvedValue({ owner: 'seek-oss', repo: 'skuba' });

const files = { 'renovate.json5': JSON5 };

vol.fromJSON(files);

await expect(tryPatchRenovateConfig()).resolves.toBeUndefined();

expect(volToJson()).toStrictEqual(files);

expect(writeFile).not.toHaveBeenCalled();
});

it('skips a personal project', async () => {
getOwnerAndRepo.mockResolvedValue({ owner: 'Seekie1337', repo: 'fizz-buzz' });

const files = { '.renovaterc': JSON };

vol.fromJSON(files);

await expect(tryPatchRenovateConfig()).resolves.toBeUndefined();

expect(volToJson()).toStrictEqual(files);

expect(writeFile).not.toHaveBeenCalled();
});

it('skips a strange config without `extends`', async () => {
getOwnerAndRepo.mockResolvedValue({ owner: 'SEEK-Jobs', repo: 'monolith' });

const files = { '.github/renovate.json5': '{}' };

vol.fromJSON(files);

await expect(tryPatchRenovateConfig()).resolves.toBeUndefined();

expect(volToJson()).toStrictEqual(files);

expect(writeFile).not.toHaveBeenCalled();
});

it('skips a configured SEEK-Jobs project', async () => {
getOwnerAndRepo.mockResolvedValue({ owner: 'SEEK-Jobs', repo: 'monolith' });

const files = { '.github/renovate.json5': JSON5_CONFIGURED };

vol.fromJSON(files);

await expect(tryPatchRenovateConfig()).resolves.toBeUndefined();

expect(volToJson()).toStrictEqual(files);

expect(writeFile).not.toHaveBeenCalled();
});
143 changes: 143 additions & 0 deletions src/cli/configure/patchRenovateConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/* eslint-disable new-cap */

import { inspect } from 'util';

import fs from 'fs-extra';
import * as fleece from 'golden-fleece';
import * as t from 'runtypes';

import * as Git from '../../api/git';
import { log } from '../../utils/logging';

import { createDestinationFileReader } from './analysis/project';
import { RENOVATE_CONFIG_FILENAMES } from './modules/renovate';
import { formatPrettier } from './processing/prettier';

const RENOVATE_PRESETS = [
'local>seekasia/renovate-config',
'local>seek-jobs/renovate-config',
] as const;

type RenovateFiletype = 'json' | 'json5';

type RenovatePreset = (typeof RENOVATE_PRESETS)[number];

const RenovateConfig = t.Record({
extends: t.Array(t.String),
});

const ownerToRenovatePreset = (owner: string): RenovatePreset | undefined => {
const lowercaseOwner = owner.toLowerCase();

switch (lowercaseOwner) {
case 'seekasia':
return 'local>seekasia/renovate-config';

case 'seek-jobs':
return 'local>seek-jobs/renovate-config';

default:
return;
}
};

type PatchFile = (props: {
filepath: string;
input: string;
presetToAdd: RenovatePreset;
}) => Promise<void>;

const patchJson: PatchFile = async ({ filepath, input, presetToAdd }) => {
const config: unknown = JSON.parse(input);

if (!RenovateConfig.guard(config)) {
return;
}

config.extends.unshift(presetToAdd);

await fs.promises.writeFile(
filepath,
formatPrettier(JSON.stringify(config), { parser: 'json' }),
);

return;
};

const patchJson5: PatchFile = async ({ filepath, input, presetToAdd }) => {
const config: unknown = fleece.evaluate(input);

if (!RenovateConfig.guard(config)) {
return;
}

config.extends.unshift(presetToAdd);

await fs.promises.writeFile(
filepath,
formatPrettier(fleece.patch(input, config), { parser: 'json5' }),
);

return;
};

const patchByFiletype: Record<RenovateFiletype, PatchFile> = {
json: patchJson,
json5: patchJson5,
};

const patchRenovateConfig = async (dir: string) => {
const readFile = createDestinationFileReader(dir);

const { owner } = await Git.getOwnerAndRepo({ dir });

const presetToAdd = ownerToRenovatePreset(owner);

if (!presetToAdd) {
// No baseline preset needs to be added for the configured Git owner.
return;
}

const maybeConfigs = await Promise.all(
RENOVATE_CONFIG_FILENAMES.map(async (filepath) => ({
input: await readFile(filepath),
filepath,
})),
);

const config = maybeConfigs.find((maybeConfig) => Boolean(maybeConfig.input));

if (
// No file was found.
!config?.input ||
// The file appears to mention the baseline preset for the configured Git
// owner. This is a very naive check that we don't want to overcomplicate
// because it is invoked before each skuba format and lint.
config.input.includes(presetToAdd)
) {
return;
}

const filetype: RenovateFiletype = config.filepath
.toLowerCase()
.endsWith('.json5')
? 'json5'
: 'json';

const patchFile = patchByFiletype[filetype];

await patchFile({
filepath: config.filepath,
input: config.input,
presetToAdd,
});
};

export const tryPatchRenovateConfig = async (dir = process.cwd()) => {
try {
await patchRenovateConfig(dir);
} catch (err) {
log.warn('Failed to patch Renovate config.');
log.subtle(inspect(err));
}
};
6 changes: 6 additions & 0 deletions src/cli/format.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ jest
.spyOn(console, 'log')
.mockImplementation((...args) => stdoutMock(`${args.join(' ')}\n`));

jest
.spyOn(git, 'listRemotes')
.mockResolvedValue([
{ remote: 'origin', url: '[email protected]:seek-oss/skuba.git' },
]);

const SOURCE_FILES = ['a/a/a.ts', 'b.md', 'c.json', 'd.js'];

const BASE_PATH = path.join(__dirname, '..', '..', 'integration', 'base');
Expand Down
Loading

0 comments on commit dbd4853

Please sign in to comment.