Files
renovate/lib/util/git/errors.spec.ts
T
Sebastian Poxhofer 6e03e8326f refactor(linters): add rule to enforce inline single use fixtures (#45230)
* feat(lint): add renovate/prefer-is-helpers and renovate/prefer-is-object rules

Flag ad-hoc type checks that have @sindresorhus/is equivalents:
- .filter(Boolean) -> .filter(isTruthy)
- typeof x === 'string' -> isString(x)
- x === null || x === undefined -> isNullOrUndefined(x) (and negation)

The typeof x === 'object' shape lives in a separate suggestion-only rule
(renovate/prefer-is-object, kept at warn) because isObject()/isPlainObject()
are not drop-in equivalents: null, arrays and functions behave differently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: fix renovate/prefer-is-helpers violations

Replace ad-hoc type checks with @sindresorhus/is helpers across lib/:
- 13x .filter(Boolean) -> .filter(isTruthy)
- 36x typeof x ==/!== 'string' -> isString(x) / !isString(x)
- 6x x === null || x === undefined -> isNullOrUndefined(x) (and negation)

Promote renovate/prefer-is-helpers to error now that the codebase is clean.
renovate/prefer-is-object stays at warn: its 15 remaining sites guard
parsed YAML/JSON or unknown values where isObject()/isPlainObject() are
not provably equivalent (null/array/function semantics).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: enforce renovate/prefer-is-object at error level

Resolve the 15 remaining warnings and raise the rule from warn to error:

- Migrate 10 sites to isObject() where the guarded value provably cannot
  be a function (parsed YAML/JSON, typed exec command arguments, or
  test-controlled fixtures): helm-values, woodpecker, bun, pre-commit
  managers, exec utils, file package cache, and the github http spec.
  In the bun manager this also fixes a latent crash where a null
  `workspaces` value would throw on the `in` operator.
- Keep 5 sites as reasoned oxlint-disable comments where excluding
  functions is load-bearing: the byte-exact fingerprint canonicalizer
  (2), the circularity walker, the zod result guard, and the generic
  hasKey helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(lint): add renovate/inline-single-use-fixtures rule

In spec files, a module-scope const fixture that is referenced from
exactly one it()/test() block should be inlined into that test so the
test is self-contained. The rule is a warn-level nudge (naming for
clarity is a valid exception) with no autofix (moving code is risky).

Uses oxlint jsPlugins scope analysis (getDeclaredVariables) for exact,
shadowing-safe reference counting and bails out conservatively: only
top-level non-exported single-declarator consts with multiline,
non-function initializers whose every reference sits inside the callback
function of one and the same test call are flagged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: inline single-use spec fixtures into their tests

Fix 26 renovate/inline-single-use-fixtures warnings in specs where the
move is mechanical and value-preserving: codeBlock fixtures were
re-indented uniformly (codeBlock strips the common indentation, so the
produced string is unchanged) and Fixtures.get() calls resolve relative
to the spec file, so relocating them inside the test body has no effect.

The remaining 59 warnings are left for per-site judgment, as the rule is
a warn-level nudge by design.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: inline remaining relocation-safe single-use spec fixtures

Move the 24 remaining single-use module-scope fixtures whose initializer
is a codeBlock tagged template or a Fixtures accessor call into the one
it() block that uses them. Both shapes are value-preserving to relocate:
codeBlock strips the common leading indentation, so the uniformly
re-indented templates produce identical strings, and Fixtures.get()
resolves relative to the spec file, not the call site's position in it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(lint): enforce inline-single-use-fixtures as error

Narrow the rule to the initializer shapes where moving the declaration
into the test body is provably value-preserving and unambiguously an
improvement: codeBlock/stripIndent tagged templates (indentation-
normalizing, so re-indenting is safe) and Fixtures accessor calls
(resolve relative to the spec file). Plain object/array literals,
untagged template literals (whose leading-newline formatting would need
ugly re-indentation) and arbitrary call expressions remain per-site
judgment calls and are no longer flagged.

With the rule reduced to true defects and all remaining violations
inlined, bump it from warn to error in the spec override block.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: inline contents of fixtures relocated into single-use tests

The previous commits moved single-use module-scope Fixtures.get() consts
into their one it() block, which only relocated the file indirection.
Replace those relocated Fixtures.get() calls with the fixture file's
actual content — codeBlock tagged templates for text fixtures — so the
tests are self-contained, and delete the fixture files that no longer
have any references. Relocated fixtures too large to inline comfortably
(terraform-provider releases, sbt private-variable file, nuget v2 pkg
list, bundled package-lock) keep their Fixtures.get() calls.

Value preservation was verified by unchanged vitest results for every
touched spec; all consumers parse the content, so codeBlock's trailing
newline normalization is behavior-neutral.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: inline remaining large single-use fixtures

Inline the four Fixtures.get() sites that were previously exempted for
size, so every single-use fixture is now inlined regardless of size:

- nuget v2_withoutProjectUrl.xml -> codeBlock (served via http mock)
- terraform-provider releaseBackendIndexGoogleBeta.json -> plain object
  literal passed to the nock reply
- npm locked-dependency bundled.package-lock.json -> codeBlock JSON
  string (consumed as raw lockFileContent)
- sbt private-variable-dependency-file.scala -> codeBlock

All consumers parse the content, so codeBlock's trailing-newline
normalization is behavior-neutral; vitest results are unchanged for
every touched spec, including the two pre-existing nuget failures.

The nuget, terraform-provider and sbt fixture files have no remaining
references and are deleted. bundled.package-lock.json is kept because
package-lock/get-locked.spec.ts still reads it via Fixtures.getJson().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: keep large bundled package-lock fixture in a file

The bundled package-lock.json inlined into the "fails remediation if
bundled" test is ~150 KB (~4,400 lines); inlining it as a codeBlock
hurts readability more than it helps. Read it via Fixtures.get inside
the test block instead — the inline-single-use-fixtures rule permits a
Fixtures.get() call in test scope, and the fixture file is still present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-03 07:42:45 +00:00

89 lines
3.9 KiB
TypeScript

import { codeBlock } from 'common-tags';
import { CONFIG_VALIDATION } from '../../constants/error-messages.ts';
import { bulkChangesDisallowed, handleCommitError } from './error.ts';
import type { FileChange } from './types.ts';
const workflowFile: FileChange = {
type: 'addition',
path: '.github/workflows/build.yaml',
contents: '',
};
describe('util/git/errors', () => {
describe('bulkChangesDisallowed', () => {
it('should match the expected error', () => {
const errorMsg = codeBlock`
To https://github.com/the-org/st-mono.git
!\t:refs/renovate/branches/renovate/foo\t[remote failure] (remote failed to report status)
!\t:refs/renovate/branches/renovate/bar\t[remote failure] (remote failed to report status)
Done
Pushing to https://github.com/foo/bar.git
POST git-receive-pack (1234 bytes)
remote: Repository policies do not allow pushes that update more than 2 branches or tags.
error: failed to push some refs to 'https://github.com/foo/bar.git'
`;
const err = new Error(errorMsg);
expect(bulkChangesDisallowed(err)).toBe(true);
});
});
describe('handleCommitError', () => {
it('throws a CONFIG_VALIDATION error when GitHub returns GH013', () => {
// Real-world GH013 error from a GitHub Push Protection ruleset that
// restricts writes to files under .github/workflows/. Captured from a
// self-hosted Renovate run.
const gh013PushProtectionErr = codeBlock`
To https://github.com/the-org/example.git
! refs/renovate/branches/renovate/go-1.x:refs/renovate/branches/renovate/go-1.x [remote rejected] (push declined due to repository rule violations)
Done
Pushing to https://github.com/the-org/example.git
POST git-receive-pack (734 bytes)
remote: error: GH013: Repository rule violations found for refs/renovate/branches/renovate/go-1.x.
remote: Review all repository rules at https://github.com/the-org/example/rules?ref=refs%2Frenovate%2Fbranches%2Frenovate%2Fgo-1.x
remote:
remote: - GITHUB PUSH PROTECTION
remote: ---------------------------------------
remote: Resolve the following violations before pushing again
remote:
remote: - File path is restricted
remote: Found 2 violations:
remote:
remote: .github/workflows/build.yaml
remote: .github/workflows/deploy.yaml
remote:
error: failed to push some refs to 'https://github.com/the-org/example.git'
`;
const err = new Error(gh013PushProtectionErr);
// Even when every changed file is under .github/workflows/, which
// previously triggered a silent info-level abort, GH013 must be
// surfaced so the repository is flagged as failing.
let thrown: any;
try {
handleCommitError(err, 'renovate/go-1.x', [workflowFile]);
} catch (e) {
thrown = e;
}
expect(thrown).toBeDefined();
expect(thrown.message).toBe(CONFIG_VALIDATION);
expect(thrown.validationSource).toBe('renovate/go-1.x');
expect(thrown.validationError).toBe(
'GitHub repository ruleset violation',
);
expect(thrown.validationMessage).toContain('GH013');
expect(thrown.validationMessage).toContain('renovate/go-1.x');
});
it('still silently aborts on generic workflow-file push rejection', () => {
// Sanity check: non-GH013 workflow rejection should still be
// swallowed so we do not regress the existing behaviour where
// App tokens without the `workflows` scope abort quietly.
const err = new Error(codeBlock`
remote rejected refs/renovate/branches/foo
error: failed to push some refs to 'https://github.com/the-org/example.git'
`);
const result = handleCommitError(err, 'renovate/foo', [workflowFile]);
expect(result).toBeNull();
});
});
});