Files
renovate/lib/modules/platform/azure/azure-helper.ts
Sebastian Poxhofer e0cec3dc95 refactor(lint): add types-location rule and enable it for lib/modules (#45497)
feat(lint): add `types-location` rule and enable it for `lib/modules`

Reviewers consistently ask for exported types to be moved into a
colocated `types.ts`. Add a `renovate/types-location` oxlint rule that
reports `export interface` / `export type` declarations outside such a
file, and turn it on across `lib/modules`.

Re-exports (`export type { X }`, `export type *`), file-local types, and
values (enums, classes, functions) are left alone. `schema.ts` files and
`schema/` directories are excluded via config, matching how
`zod-schema-location` is configured — inferred types belong next to the
schema they are derived from.

Relocating the 87 existing violations means:

- new `types.ts` files for the modules that had none
- `terragrunt/common.ts` folded into its `types.ts`, as it held nothing
  else
- `RangeBound` in `versioning/gradle/compare.ts` un-exported, since only
  a file-local interface referenced it
- `bazel-module/parser/fragments.ts` split from its inferred types, which
  now live in `parser/types.ts` and infer through a namespace import so
  each schema keeps its merged name
2026-08-27 19:52:09 +00:00

233 lines
6.8 KiB
TypeScript

import type { WebApiTeam } from 'azure-devops-node-api/interfaces/CoreInterfaces.js';
import type {
GitCommit,
GitRef,
} from 'azure-devops-node-api/interfaces/GitInterfaces.js';
import { GitPullRequestMergeStrategy } from 'azure-devops-node-api/interfaces/GitInterfaces.js';
import type {
PolicyConfiguration,
PolicyEvaluationRecord,
} from 'azure-devops-node-api/interfaces/PolicyInterfaces.js';
import { logger } from '../../../logger/index.ts';
import { streamToString } from '../../../util/streams.ts';
import { getNewBranchName } from '../util.ts';
import * as azureApi from './azure-got-wrapper.ts';
import { WrappedException } from './schema.ts';
import type { AzureBranchObj } from './types.ts';
import {
getBranchNameWithoutRefsPrefix,
getBranchNameWithoutRefsheadsPrefix,
} from './util.ts';
const mergePolicyGuid = 'fa4e907d-c16b-4a4c-9dfa-4916e5d171ab'; // Magic GUID for merge strategy policy configurations
export async function getRefs(
repoId: string,
branchName?: string,
): Promise<GitRef[]> {
logger.debug(`getRefs(${repoId}, ${branchName!})`);
const azureApiGit = await azureApi.gitApi();
const refs = await azureApiGit.getRefs(
repoId,
undefined,
getBranchNameWithoutRefsPrefix(branchName),
);
return refs;
}
export async function getAzureBranchObj(
repoId: string,
branchName: string,
from?: string,
): Promise<AzureBranchObj> {
const fromBranchName = getNewBranchName(from);
const refs = await getRefs(repoId, fromBranchName);
if (refs.length === 0) {
logger.debug(`getAzureBranchObj without a valid from, so initial commit.`);
// TODO: fix undefined
return {
name: getNewBranchName(branchName)!,
oldObjectId: '0000000000000000000000000000000000000000',
};
}
return {
// TODO: fix undefined (#22198)
name: getNewBranchName(branchName)!,
oldObjectId: refs[0].objectId!,
};
}
// if no branchName, look globally
export async function getFile(
repoId: string,
filePath: string,
branchName: string,
): Promise<string | null> {
logger.trace(`getFile(filePath=${filePath}, branchName=${branchName})`);
const azureApiGit = await azureApi.gitApi();
const item = await azureApiGit.getItemText(
repoId,
filePath,
undefined,
undefined,
0, // because we look for 1 file
false,
false,
true,
{
versionType: 0, // branch
versionOptions: 0,
version: getBranchNameWithoutRefsheadsPrefix(branchName),
},
);
if (item?.readable) {
const fileContent = await streamToString(item);
try {
const result = WrappedException.safeParse(fileContent);
if (result.success) {
if (result.data.typeKey === 'GitItemNotFoundException') {
logger.warn({ filePath }, 'Unable to find file');
return null;
}
// v8 ignore else -- TODO: add test #40625
if (result.data.typeKey === 'GitUnresolvableToCommitException') {
logger.warn({ branchName }, 'Unable to find branch');
return null;
}
}
} catch /* v8 ignore next -- non-JSON error body falls through to raw file content, parse failure not simulated in specs */ {
// it 's not a JSON, so I send the content directly with the line under
}
return fileContent;
}
return null; // no file found
}
export async function getCommitDetails(
commit: string,
repoId: string,
): Promise<GitCommit> {
logger.debug(`getCommitDetails(${commit}, ${repoId})`);
const azureApiGit = await azureApi.gitApi();
const results = await azureApiGit.getCommit(commit, repoId);
return results;
}
interface MergeStrategyPolicyConfiguration {
allowNoFastForward?: boolean;
allowSquash?: boolean;
allowRebase?: boolean;
allowRebaseMerge?: boolean;
}
const policyKeyByStrategy: Record<
GitPullRequestMergeStrategy,
keyof MergeStrategyPolicyConfiguration
> = {
[GitPullRequestMergeStrategy.NoFastForward]: 'allowNoFastForward',
[GitPullRequestMergeStrategy.Squash]: 'allowSquash',
[GitPullRequestMergeStrategy.Rebase]: 'allowRebase',
[GitPullRequestMergeStrategy.RebaseMerge]: 'allowRebaseMerge',
};
export async function getMergeMethod(
repoId: string,
project: string,
branchRef?: string | null,
defaultBranch?: string,
): Promise<GitPullRequestMergeStrategy> {
logger.debug(
`getMergeMethod(branchRef=${branchRef}, defaultBranch=${defaultBranch})`,
);
interface Scope {
repositoryId: string;
refName?: string;
matchKind: 'Prefix' | 'Exact' | 'DefaultBranch';
}
function isRelevantScope(scope: Scope): boolean {
if (
scope.matchKind === 'DefaultBranch' &&
// TODO: types (#22198)
(!branchRef || branchRef === `refs/heads/${defaultBranch!}`)
) {
return true;
}
if (scope.repositoryId !== repoId && scope.repositoryId !== null) {
return false;
}
if (!branchRef) {
return true;
}
// TODO #22198
return scope.matchKind === 'Exact'
? scope.refName === branchRef
: branchRef.startsWith(scope.refName!);
}
const policyConfigurations = (
await (
await azureApi.policyApi()
).getPolicyConfigurations(project, undefined, mergePolicyGuid)
)
.filter((p: PolicyConfiguration) => p.settings.scope.some(isRelevantScope))
.map((p: PolicyConfiguration) => p.settings)[0];
logger.debug(
{ policyConfigurations },
// TODO: types (#22198)
`getMergeMethod(branchRef=${branchRef!}) determining mergeMethod from matched policy`,
);
// Note that this will iterate in the order of GitPullRequestMergeStrategy
for (const [key, policyKey] of Object.entries(policyKeyByStrategy)) {
if (policyConfigurations?.[policyKey] === true) {
const method = parseInt(key, 10) satisfies GitPullRequestMergeStrategy;
logger.debug(
{ policyConfigurations },
`getMergeMethod(branchRef=${branchRef!})=${GitPullRequestMergeStrategy[method]}`,
);
return method;
}
}
logger.debug(
{ policyConfigurations },
// TODO: types (#22198)
`getMergeMethod(branchRef=${branchRef!})=${GitPullRequestMergeStrategy[GitPullRequestMergeStrategy.NoFastForward]}`,
);
return GitPullRequestMergeStrategy.NoFastForward;
}
export async function getPolicyEvaluations(
project: string,
artifactId: string,
): Promise<PolicyEvaluationRecord[]> {
logger.debug(`getPolicyEvaluations(${project}, ${artifactId})`);
const policyEvaluations = await (
await azureApi.policyApi()
).getPolicyEvaluations(project, artifactId);
return policyEvaluations;
}
export async function getAllProjectTeams(
projectId: string,
): Promise<WebApiTeam[]> {
const allTeams: WebApiTeam[] = [];
const azureApiCore = await azureApi.coreApi();
const top = 100;
let skip = 0;
let length = 0;
do {
const teams = await azureApiCore.getTeams(projectId, undefined, top, skip);
length = teams.length;
allTeams.push(...teams);
skip += top;
} while (top <= length);
return allTeams;
}