Files
Anand Kumria 3cba608bb1 fix(git): surface GitHub GH013 ruleset rejections as config errors (#44819)
When GitHub returns 'GH013: Repository rule violations found' during
a push, the failure is currently silently swallowed by handleCommitError
whenever the changeset touches a file under .github/workflows/. The
existing swallow was intended for the narrow case of an App token
missing the 'workflows' scope, but it also masks unrelated ruleset
violations such as:

  - GITHUB PUSH PROTECTION 'File path is restricted'
  - Secret scanning push protection
  - Signed commit / branch-naming / tag-deletion rulesets

Self-hosted operators cannot see these failures in aggregated log
reports because Renovate logs at info level, marks the branch as
aborted, and continues. Repositories that are effectively 100%
blocked by a ruleset appear healthy.

Detect the 'GH013' token before the workflow-files silent-swallow and
throw a CONFIG_VALIDATION error - mirroring the treatment already
applied to GH003 (force-push denied) and 'protected branch hook
declined'. This flags the repository as failing, populates
validationError / validationMessage so the failure reason is visible
in the repository summary, and lets any downstream reporting pick it
up as a real failure instead of an info-level abort.

Adds tests to lib/util/git/errors.spec.ts covering:

  1. A GH013 Push Protection error whose changeset is entirely under
     .github/workflows/ is thrown as CONFIG_VALIDATION with the
     expected validationSource / validationError / validationMessage.

  2. A generic non-GH013 workflow-file remote rejection still returns
     null (existing swallow behaviour preserved).
2026-07-24 09:08:04 +00:00

162 lines
6.0 KiB
TypeScript

import { CONFIG_VALIDATION } from '../../constants/error-messages.ts';
import { logger } from '../../logger/index.ts';
import { ExternalHostError } from '../../types/errors/external-host-error.ts';
import { getEnv } from '../env.ts';
import type { FileChange } from './types.ts';
export function checkForPlatformFailure(err: Error): Error | null {
if (getEnv().NODE_ENV === 'test') {
return null;
}
const externalHostFailureStrings = [
'remote: Invalid username or password',
'gnutls_handshake() failed',
'The requested URL returned error: 403',
'The requested URL returned error: 5',
'The remote end hung up unexpectedly',
'access denied or repository not exported',
'Could not write new index file',
'Failed to connect to',
'Connection timed out',
'malformed object name',
'Could not resolve host',
'early EOF',
'fatal: bad config', // .gitmodules problem
'expected flush after ref listing',
];
for (const errorStr of externalHostFailureStrings) {
if (err.message.includes(errorStr)) {
logger.debug({ err }, 'Converting git error to ExternalHostError');
return new ExternalHostError(err, 'git');
}
/* v8 ignore next -- TODO: add test */
}
/* v8 ignore next -- TODO: add test */
const configErrorStrings = [
{
error: 'GitLab: Branch name does not follow the pattern',
message:
"Cannot push because branch name does not follow project's push rules",
},
{
error: 'GitLab: Commit message does not follow the pattern',
message:
"Cannot push because commit message does not follow project's push rules",
},
{
error: ' is not a member of team',
message:
'The `Restrict commits to existing GitLab users` rule is blocking Renovate push. Check the Renovate `gitAuthor` setting',
},
{
error: 'TF401027:',
message:
'You need the Git `GenericContribute` permission to perform this action',
},
{
error: 'matches more than one',
message:
"Renovate cannot push branches if there are tags with names the same as Renovate's branches. Please remove conflicting tag names or change Renovate's branchPrefix to avoid conflicts.",
},
];
/* v8 ignore next -- TODO: add test */
for (const { error, message } of configErrorStrings) {
if (err.message.includes(error)) {
logger.debug({ err }, 'Converting git error to CONFIG_VALIDATION error');
const res = new Error(CONFIG_VALIDATION);
res.validationError = message;
res.validationMessage = `\`${err.message.replaceAll('`', "'")}\``;
return res;
}
}
/* v8 ignore next -- TODO: add test */
return null;
}
/* v8 ignore next -- TODO: add tests */
export function handleCommitError(
err: Error,
branchName: string,
files?: FileChange[],
): null {
checkForPlatformFailure(err);
if (err.message.includes(`'refs/heads/renovate' exists`)) {
const error = new Error(CONFIG_VALIDATION);
error.validationSource = 'None';
error.validationError = 'An existing branch is blocking Renovate';
error.validationMessage = `Renovate needs to create the branch \`${branchName}\` but is blocked from doing so because of an existing branch called \`renovate\`. Please remove it so that Renovate can proceed.`;
throw error;
}
if (
err.message.includes(
'refusing to allow a GitHub App to create or update workflow',
)
) {
logger.warn(
'App has not been granted permissions to update Workflows - aborting branch.',
);
return null;
}
if (err.message.includes('GH013')) {
logger.debug({ err }, 'GitHub repository ruleset blocked push');
const error = new Error(CONFIG_VALIDATION);
error.validationSource = branchName;
error.validationError = 'GitHub repository ruleset violation';
error.validationMessage =
`Renovate cannot push to \`${branchName}\` because a GitHub ` +
`repository ruleset rejected the push (GH013). Update the ` +
`ruleset - or grant Renovate a bypass actor - so Renovate can ` +
`proceed. Original error: \`${err.message.replaceAll('`', "'")}\``;
throw error;
}
if (
(err.message.includes('remote rejected') || err.message.includes('403')) &&
files?.some((file) => file.path?.startsWith('.github/workflows/'))
) {
logger.debug({ err }, 'commitFiles error');
logger.info('Workflows update rejection - aborting branch.');
return null;
}
if (err.message.includes('protected branch hook declined')) {
const error = new Error(CONFIG_VALIDATION);
error.validationSource = branchName;
error.validationError = 'Renovate branch is protected';
error.validationMessage = `Renovate cannot push to its branch because branch protection has been enabled.`;
throw error;
}
if (err.message.includes('can only push your own commits')) {
const error = new Error(CONFIG_VALIDATION);
error.validationSource = branchName;
error.validationError = 'Bitbucket committer error';
error.validationMessage = `Renovate has experienced the following error when attempting to push its branch to the server: \`${err.message.replaceAll(
'`',
"'",
)}\``;
throw error;
}
if (err.message.includes('remote: error: cannot lock ref')) {
logger.error({ err }, 'Error committing files.');
return null;
}
if (
err.message.includes('denying non-fast-forward') ||
err.message.includes('GH003: Sorry, force-pushing')
) {
logger.debug({ err }, 'Permission denied to update branch');
const error = new Error(CONFIG_VALIDATION);
error.validationSource = branchName;
error.validationError = 'Force push denied';
error.validationMessage = `Renovate is unable to update branch(es) due to force pushes being disallowed.`;
throw error;
}
logger.debug({ err }, 'Unknown error committing files');
// We don't know why this happened, so this will cause bubble up to a branch error
throw err;
}
export function bulkChangesDisallowed(err: Error): boolean {
return err.message.includes('update more than');
}