Skip to content

Commit

Permalink
feat: Add branch option for postUpgradeCommands (#8725)
Browse files Browse the repository at this point in the history
* Add branch option for postUpgradeCommands

* Put the for loop back for async

* Fix tests & add documentation

* Change execution mode dependency to update

* Move postUpgradeCommand execution to seperate file

* Fix return time of upgradeTaskExecuter

* Fix test

* Finished new mode and added test

* Remove .only

* Remove defensive coding to make codecov happy

* Give inconfig a proper type

* Another missing type

* Fix typo in allowPostUpgradeCommandTemplating option description

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>

* Move to 1 postUpgradeCommandExecutor call

* Appease the import order rule gods

* Refactor more

* Okay now it should be done

* Fix bug

* Import order rule

* Change import to a type import

* Also return artifacterrors

* Apply suggestions from code review

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>

* Move checking if postUpgradeCommands can run

* remove unused import

* Fix prettier error

* Call getAdminConfig once

* Apply suggestions from code review

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>

* Change type name to EnsurePrResult

* Update lib/workers/branch/index.spec.ts

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>

* Apply suggestions from code review

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>

* Fix enum imports

Co-authored-by: Carlin St Pierre <cstpierre@atlassian.com>
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
  • Loading branch information
3 people committed Apr 9, 2021
1 parent f91a162 commit 6afbcf8
Show file tree
Hide file tree
Showing 8 changed files with 441 additions and 244 deletions.
10 changes: 8 additions & 2 deletions docs/usage/configuration-options.md
Expand Up @@ -1552,12 +1552,13 @@ e.g.
{
"postUpgradeTasks": {
"commands": ["tslint --fix"],
"fileFilters": ["yarn.lock", "**/*.js"]
"fileFilters": ["yarn.lock", "**/*.js"],
"executionMode": "update"
}
}
```

The `postUpgradeTasks` configuration consists of two fields:
The `postUpgradeTasks` configuration consists of three fields:

### commands

Expand All @@ -1567,6 +1568,11 @@ A list of commands that are executed after Renovate has updated a dependency but

A list of glob-style matchers that determine which files will be included in the final commit made by Renovate

### executionMode

Defaults to `update`, but can also be set to `branch`. This sets the level the postUpgradeTask runs on, if set to `update` the postUpgradeTask
will be executed for every dependency on the branch. If set to `branch` the postUpgradeTask is executed for the whole branch.

## prBodyColumns

Use this array to provide a list of column names you wish to include in the PR tables.
Expand Down
2 changes: 1 addition & 1 deletion docs/usage/self-hosted-configuration.md
Expand Up @@ -11,7 +11,7 @@ Please also see [Self-Hosted Experimental Options](./self-hosted-experimental.md

## allowPostUpgradeCommandTemplating

Set to true to allow templating of post-upgrade commands.
Set to true to allow templating of dependency level post-upgrade commands.

Let's look at an example of configuring packages with existing Angular migrations.

Expand Down
11 changes: 11 additions & 0 deletions lib/config/definitions.ts
Expand Up @@ -32,6 +32,7 @@ const options: RenovateOptions[] = [
default: {
commands: [],
fileFilters: [],
executionMode: 'update',
},
},
{
Expand All @@ -54,6 +55,16 @@ const options: RenovateOptions[] = [
default: [],
cli: false,
},
{
name: 'executionMode',
description:
'Controls whether the post upgrade tasks runs for every update or once per upgrade branch',
type: 'string',
parent: 'postUpgradeTasks',
allowedValues: ['update', 'branch'],
default: 'update',
cli: false,
},
{
name: 'onboardingBranch',
description:
Expand Down
2 changes: 2 additions & 0 deletions lib/config/types.ts
Expand Up @@ -114,10 +114,12 @@ export interface LegacyAdminConfig {
platform?: string;
requireConfig?: boolean;
}
export type ExecutionMode = 'branch' | 'update';

export type PostUpgradeTasks = {
commands?: string[];
fileFilters?: string[];
executionMode: ExecutionMode;
};

type UpdateConfig<
Expand Down
196 changes: 196 additions & 0 deletions lib/workers/branch/execute-post-upgrade-commands.ts
@@ -0,0 +1,196 @@
import is from '@sindresorhus/is';
import minimatch from 'minimatch';
import { getAdminConfig } from '../../config/admin';
import { addMeta, logger } from '../../logger';
import type { ArtifactError } from '../../manager/types';
import { exec } from '../../util/exec';
import { readLocalFile, writeLocalFile } from '../../util/fs';
import { File, getRepoStatus } from '../../util/git';
import { regEx } from '../../util/regex';
import { sanitize } from '../../util/sanitize';
import { compile } from '../../util/template';
import type { BranchConfig, BranchUpgradeConfig } from '../types';

export type PostUpgradeCommandsExecutionResult = {
updatedArtifacts: File[];
artifactErrors: ArtifactError[];
};

export async function postUpgradeCommandsExecutor(
filteredUpgradeCommands: BranchUpgradeConfig[],
config: BranchConfig
): Promise<PostUpgradeCommandsExecutionResult> {
let updatedArtifacts = [...(config.updatedArtifacts || [])];
const artifactErrors = [...(config.artifactErrors || [])];
const {
allowedPostUpgradeCommands,
allowPostUpgradeCommandTemplating,
} = getAdminConfig();

for (const upgrade of filteredUpgradeCommands) {
addMeta({ dep: upgrade.depName });
logger.trace(
{
tasks: upgrade.postUpgradeTasks,
allowedCommands: allowedPostUpgradeCommands,
},
`Checking for post-upgrade tasks`
);
const commands = upgrade.postUpgradeTasks?.commands || [];
const fileFilters = upgrade.postUpgradeTasks?.fileFilters || [];

if (is.nonEmptyArray(commands)) {
// Persist updated files in file system so any executed commands can see them
for (const file of config.updatedPackageFiles.concat(updatedArtifacts)) {
if (file.name !== '|delete|') {
let contents;
if (typeof file.contents === 'string') {
contents = Buffer.from(file.contents);
} else {
contents = file.contents;
}
await writeLocalFile(file.name, contents);
}
}

for (const cmd of commands) {
if (
allowedPostUpgradeCommands.some((pattern) => regEx(pattern).test(cmd))
) {
try {
const compiledCmd = allowPostUpgradeCommandTemplating
? compile(cmd, upgrade)
: cmd;

logger.debug({ cmd: compiledCmd }, 'Executing post-upgrade task');
const execResult = await exec(compiledCmd, {
cwd: config.localDir,
});

logger.debug(
{ cmd: compiledCmd, ...execResult },
'Executed post-upgrade task'
);
} catch (error) {
artifactErrors.push({
lockFile: upgrade.packageFile,
stderr: sanitize(error.message),
});
}
} else {
logger.warn(
{
cmd,
allowedPostUpgradeCommands,
},
'Post-upgrade task did not match any on allowed list'
);
artifactErrors.push({
lockFile: upgrade.packageFile,
stderr: sanitize(
`Post-upgrade command '${cmd}' does not match allowed pattern${
allowedPostUpgradeCommands.length === 1 ? '' : 's'
} ${allowedPostUpgradeCommands.map((x) => `'${x}'`).join(', ')}`
),
});
}
}

const status = await getRepoStatus();

for (const relativePath of status.modified.concat(status.not_added)) {
for (const pattern of fileFilters) {
if (minimatch(relativePath, pattern)) {
logger.debug(
{ file: relativePath, pattern },
'Post-upgrade file saved'
);
const existingContent = await readLocalFile(relativePath);
const existingUpdatedArtifacts = updatedArtifacts.find(
(ua) => ua.name === relativePath
);
if (existingUpdatedArtifacts) {
existingUpdatedArtifacts.contents = existingContent;
} else {
updatedArtifacts.push({
name: relativePath,
contents: existingContent,
});
}
// If the file is deleted by a previous post-update command, remove the deletion from updatedArtifacts
updatedArtifacts = updatedArtifacts.filter(
(ua) => ua.name !== '|delete|' || ua.contents !== relativePath
);
}
}
}

for (const relativePath of status.deleted || []) {
for (const pattern of fileFilters) {
if (minimatch(relativePath, pattern)) {
logger.debug(
{ file: relativePath, pattern },
'Post-upgrade file removed'
);
updatedArtifacts.push({
name: '|delete|',
contents: relativePath,
});
// If the file is created or modified by a previous post-update command, remove the modification from updatedArtifacts
updatedArtifacts = updatedArtifacts.filter(
(ua) => ua.name !== relativePath
);
}
}
}
}
}
return { updatedArtifacts, artifactErrors };
}

export default async function executePostUpgradeCommands(
config: BranchConfig
): Promise<PostUpgradeCommandsExecutionResult | null> {
const { allowedPostUpgradeCommands } = getAdminConfig();

const hasChangedFiles =
config.updatedPackageFiles?.length > 0 ||
config.updatedArtifacts?.length > 0;

if (
/* Only run post-upgrade tasks if there are changes to package files... */
!hasChangedFiles ||
is.emptyArray(allowedPostUpgradeCommands)
) {
return null;
}

const branchUpgradeCommands: BranchUpgradeConfig[] = [
{
depName: config.upgrades.map(({ depName }) => depName).join(' '),
branchName: config.branchName,
postUpgradeTasks:
config.postUpgradeTasks.executionMode === 'branch'
? config.postUpgradeTasks
: undefined,
fileFilters: config.fileFilters,
},
];

const updateUpgradeCommands: BranchUpgradeConfig[] = config.upgrades.filter(
({ postUpgradeTasks }) =>
!postUpgradeTasks ||
!postUpgradeTasks.executionMode ||
postUpgradeTasks.executionMode === 'update'
);

const {
updatedArtifacts,
artifactErrors,
} = await postUpgradeCommandsExecutor(updateUpgradeCommands, config);
return postUpgradeCommandsExecutor(branchUpgradeCommands, {
...config,
updatedArtifacts,
artifactErrors,
});
}

0 comments on commit 6afbcf8

Please sign in to comment.