-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
398 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'skuba': patch | ||
--- | ||
|
||
init: Include baseline SEEK `renovate-config` preset |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'); | ||
|
Oops, something went wrong.