mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-12 15:06:27 +00:00
feat(manager/azure-pipelines): Deployment jobs support (#26048)
This commit is contained in:
parent
f88353c671
commit
c39aa7e5b1
7 changed files with 359 additions and 109 deletions
|
@ -1,6 +0,0 @@
|
|||
jobs:
|
||||
- job: job_one
|
||||
steps:
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
script: 'echo Hello World'
|
|
@ -1,8 +0,0 @@
|
|||
stages:
|
||||
- stage: stage_one
|
||||
jobs:
|
||||
- job: job_one
|
||||
steps:
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
script: 'echo Hello World'
|
|
@ -1,4 +0,0 @@
|
|||
steps:
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
script: 'echo Hello World'
|
|
@ -1,3 +1,4 @@
|
|||
import { codeBlock } from 'common-tags';
|
||||
import { Fixtures } from '../../../../test/fixtures';
|
||||
import { GlobalConfig } from '../../../config/global';
|
||||
import { AzurePipelinesTasksDatasource } from '../../datasource/azure-pipelines-tasks';
|
||||
|
@ -15,9 +16,6 @@ const azurePipelines = Fixtures.get('azure-pipelines.yaml');
|
|||
const azurePipelinesNoDependency = Fixtures.get(
|
||||
'azure-pipelines-no-dependency.yaml',
|
||||
);
|
||||
const azurePipelinesStages = Fixtures.get('azure-pipelines-stages.yaml');
|
||||
const azurePipelinesJobs = Fixtures.get('azure-pipelines-jobs.yaml');
|
||||
const azurePipelinesSteps = Fixtures.get('azure-pipelines-steps.yaml');
|
||||
|
||||
describe('modules/manager/azure-pipelines/extract', () => {
|
||||
afterEach(() => {
|
||||
|
@ -58,12 +56,11 @@ describe('modules/manager/azure-pipelines/extract', () => {
|
|||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when reference is not defined', () => {
|
||||
it('should return null when reference is not defined specified', () => {
|
||||
expect(
|
||||
extractRepository({
|
||||
type: 'github',
|
||||
name: 'user/repo',
|
||||
ref: null,
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
@ -138,10 +135,6 @@ describe('modules/manager/azure-pipelines/extract', () => {
|
|||
datasource: 'docker',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null if image field is missing', () => {
|
||||
expect(extractContainer({ image: null })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractAzurePipelinesTasks()', () => {
|
||||
|
@ -191,11 +184,196 @@ describe('modules/manager/azure-pipelines/extract', () => {
|
|||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should extract deployment jobs runonce', () => {
|
||||
const packageFile = codeBlock`
|
||||
jobs:
|
||||
- deployment: deployment_one
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
script: 'echo Hello World'
|
||||
`;
|
||||
const res = extractPackageFile(packageFile, azurePipelinesFilename);
|
||||
expect(res?.deps).toEqual([
|
||||
{
|
||||
depName: 'Bash',
|
||||
currentValue: '3',
|
||||
datasource: AzurePipelinesTasksDatasource.id,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should extract deployment jobs on failure', () => {
|
||||
const packageFile = codeBlock`
|
||||
jobs:
|
||||
- deployment: deployment_one
|
||||
strategy:
|
||||
runOnce:
|
||||
on:
|
||||
failure:
|
||||
steps:
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
script: 'echo Hello World'
|
||||
`;
|
||||
const res = extractPackageFile(packageFile, azurePipelinesFilename);
|
||||
expect(res?.deps).toEqual([
|
||||
{
|
||||
depName: 'Bash',
|
||||
currentValue: '3',
|
||||
datasource: AzurePipelinesTasksDatasource.id,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should extract deployment jobs on success', () => {
|
||||
const packageFile = codeBlock`
|
||||
jobs:
|
||||
- deployment: deployment_one
|
||||
strategy:
|
||||
runOnce:
|
||||
on:
|
||||
success:
|
||||
steps:
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
script: 'echo Hello World'
|
||||
`;
|
||||
const res = extractPackageFile(packageFile, azurePipelinesFilename);
|
||||
expect(res?.deps).toEqual([
|
||||
{
|
||||
depName: 'Bash',
|
||||
currentValue: '3',
|
||||
datasource: AzurePipelinesTasksDatasource.id,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should extract deployment jobs postroute', () => {
|
||||
const packageFile = codeBlock`
|
||||
jobs:
|
||||
- deployment: deployment_one
|
||||
strategy:
|
||||
runOnce:
|
||||
postRouteTraffic:
|
||||
steps:
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
script: 'echo Hello World'
|
||||
`;
|
||||
const res = extractPackageFile(packageFile, azurePipelinesFilename);
|
||||
expect(res?.deps).toEqual([
|
||||
{
|
||||
depName: 'Bash',
|
||||
currentValue: '3',
|
||||
datasource: AzurePipelinesTasksDatasource.id,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should extract deployment jobs predeploy', () => {
|
||||
const packageFile = codeBlock`
|
||||
jobs:
|
||||
- deployment: deployment_one
|
||||
strategy:
|
||||
runOnce:
|
||||
preDeploy:
|
||||
steps:
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
script: 'echo Hello World'
|
||||
`;
|
||||
const res = extractPackageFile(packageFile, azurePipelinesFilename);
|
||||
expect(res?.deps).toEqual([
|
||||
{
|
||||
depName: 'Bash',
|
||||
currentValue: '3',
|
||||
datasource: AzurePipelinesTasksDatasource.id,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should extract deployment jobs route', () => {
|
||||
const packageFile = codeBlock`
|
||||
jobs:
|
||||
- deployment: deployment_one
|
||||
strategy:
|
||||
runOnce:
|
||||
routeTraffic:
|
||||
steps:
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
script: 'echo Hello World'
|
||||
`;
|
||||
const res = extractPackageFile(packageFile, azurePipelinesFilename);
|
||||
expect(res?.deps).toEqual([
|
||||
{
|
||||
depName: 'Bash',
|
||||
currentValue: '3',
|
||||
datasource: AzurePipelinesTasksDatasource.id,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should extract deployment jobs rolling', () => {
|
||||
const packageFile = codeBlock`
|
||||
jobs:
|
||||
- deployment: deployment_one
|
||||
strategy:
|
||||
rolling:
|
||||
deploy:
|
||||
steps:
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
script: 'echo Hello World'
|
||||
`;
|
||||
const res = extractPackageFile(packageFile, azurePipelinesFilename);
|
||||
expect(res?.deps).toEqual([
|
||||
{
|
||||
depName: 'Bash',
|
||||
currentValue: '3',
|
||||
datasource: AzurePipelinesTasksDatasource.id,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should extract deployment jobs canary', () => {
|
||||
const packageFile = codeBlock`
|
||||
jobs:
|
||||
- deployment: deployment_one
|
||||
strategy:
|
||||
canary:
|
||||
deploy:
|
||||
steps:
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
script: 'echo Hello World'
|
||||
`;
|
||||
const res = extractPackageFile(packageFile, azurePipelinesFilename);
|
||||
expect(res?.deps).toEqual([
|
||||
{
|
||||
depName: 'Bash',
|
||||
currentValue: '3',
|
||||
datasource: AzurePipelinesTasksDatasource.id,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should extract stages', () => {
|
||||
const res = extractPackageFile(
|
||||
azurePipelinesStages,
|
||||
azurePipelinesFilename,
|
||||
);
|
||||
const packageFile = codeBlock`
|
||||
stages:
|
||||
- stage: stage_one
|
||||
jobs:
|
||||
- job: job_one
|
||||
steps:
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
script: 'echo Hello World'
|
||||
`;
|
||||
const res = extractPackageFile(packageFile, azurePipelinesFilename);
|
||||
expect(res?.deps).toEqual([
|
||||
{
|
||||
depName: 'Bash',
|
||||
|
@ -206,10 +384,15 @@ describe('modules/manager/azure-pipelines/extract', () => {
|
|||
});
|
||||
|
||||
it('should extract jobs', () => {
|
||||
const res = extractPackageFile(
|
||||
azurePipelinesJobs,
|
||||
azurePipelinesFilename,
|
||||
);
|
||||
const packageFile = codeBlock`
|
||||
jobs:
|
||||
- job: job_one
|
||||
steps:
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
script: 'echo Hello World'
|
||||
`;
|
||||
const res = extractPackageFile(packageFile, azurePipelinesFilename);
|
||||
expect(res?.deps).toEqual([
|
||||
{
|
||||
depName: 'Bash',
|
||||
|
@ -220,10 +403,13 @@ describe('modules/manager/azure-pipelines/extract', () => {
|
|||
});
|
||||
|
||||
it('should extract steps', () => {
|
||||
const res = extractPackageFile(
|
||||
azurePipelinesSteps,
|
||||
azurePipelinesFilename,
|
||||
);
|
||||
const packageFile = codeBlock`
|
||||
steps:
|
||||
- task: Bash@3
|
||||
inputs:
|
||||
script: 'echo Hello World'
|
||||
`;
|
||||
const res = extractPackageFile(packageFile, azurePipelinesFilename);
|
||||
expect(res?.deps).toEqual([
|
||||
{
|
||||
depName: 'Bash',
|
||||
|
@ -234,10 +420,11 @@ describe('modules/manager/azure-pipelines/extract', () => {
|
|||
});
|
||||
|
||||
it('should return null when task alias used', () => {
|
||||
const content = `
|
||||
const packageFile = codeBlock`
|
||||
steps:
|
||||
- bash: 'echo Hello World'`;
|
||||
const res = extractPackageFile(content, azurePipelinesFilename);
|
||||
- bash: 'echo Hello World';
|
||||
`;
|
||||
const res = extractPackageFile(packageFile, azurePipelinesFilename);
|
||||
expect(res).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,12 +3,21 @@ import { logger } from '../../../logger';
|
|||
import { coerceArray } from '../../../util/array';
|
||||
import { regEx } from '../../../util/regex';
|
||||
import { joinUrlParts } from '../../../util/url';
|
||||
import { parseSingleYaml } from '../../../util/yaml';
|
||||
import { AzurePipelinesTasksDatasource } from '../../datasource/azure-pipelines-tasks';
|
||||
import { GitTagsDatasource } from '../../datasource/git-tags';
|
||||
import { getDep } from '../dockerfile/extract';
|
||||
import type { PackageDependency, PackageFileContent } from '../types';
|
||||
import type { AzurePipelines, Container, Repository } from './types';
|
||||
import {
|
||||
AzurePipelines,
|
||||
AzurePipelinesYaml,
|
||||
Container,
|
||||
Deploy,
|
||||
Deployment,
|
||||
Job,
|
||||
Jobs,
|
||||
Repository,
|
||||
Step,
|
||||
} from './schema';
|
||||
|
||||
const AzurePipelinesTaskRegex = regEx(/^(?<name>[^@]+)@(?<version>.*)$/);
|
||||
|
||||
|
@ -68,10 +77,6 @@ export function extractRepository(
|
|||
export function extractContainer(
|
||||
container: Container,
|
||||
): PackageDependency | null {
|
||||
if (!container.image) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dep = getDep(container.image);
|
||||
logger.debug(
|
||||
{
|
||||
|
@ -104,15 +109,60 @@ export function parseAzurePipelines(
|
|||
content: string,
|
||||
packageFile: string,
|
||||
): AzurePipelines | null {
|
||||
let pkg: AzurePipelines | null = null;
|
||||
try {
|
||||
pkg = parseSingleYaml(content, { json: true }) as AzurePipelines;
|
||||
} catch (err) /* istanbul ignore next */ {
|
||||
logger.debug({ packageFile, err }, 'Error parsing azure-pipelines content');
|
||||
const res = AzurePipelinesYaml.safeParse(content);
|
||||
if (res.success) {
|
||||
return res.data;
|
||||
} else {
|
||||
logger.debug(
|
||||
{ err: res.error, packageFile },
|
||||
'Error parsing pubspec lockfile.',
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractSteps(
|
||||
steps: Step[] | undefined,
|
||||
): PackageDependency<Record<string, any>>[] {
|
||||
const deps = [];
|
||||
for (const step of coerceArray(steps)) {
|
||||
const task = extractAzurePipelinesTasks(step.task);
|
||||
if (task) {
|
||||
deps.push(task);
|
||||
}
|
||||
}
|
||||
return deps;
|
||||
}
|
||||
|
||||
function extractJob(job: Job | undefined): PackageDependency[] {
|
||||
return extractSteps(job?.steps);
|
||||
}
|
||||
|
||||
function extractDeploy(deploy: Deploy | undefined): PackageDependency[] {
|
||||
const deps = extractJob(deploy?.deploy);
|
||||
deps.push(...extractJob(deploy?.postRouteTraffic));
|
||||
deps.push(...extractJob(deploy?.preDeploy));
|
||||
deps.push(...extractJob(deploy?.routeTraffic));
|
||||
deps.push(...extractJob(deploy?.on?.failure));
|
||||
deps.push(...extractJob(deploy?.on?.success));
|
||||
return deps;
|
||||
}
|
||||
|
||||
function extractJobs(jobs: Jobs | undefined): PackageDependency[] {
|
||||
const deps: PackageDependency[] = [];
|
||||
for (const jobOrDeployment of coerceArray(jobs)) {
|
||||
const deployment = jobOrDeployment as Deployment;
|
||||
if (deployment.strategy) {
|
||||
deps.push(...extractDeploy(deployment.strategy.canary));
|
||||
deps.push(...extractDeploy(deployment.strategy.rolling));
|
||||
deps.push(...extractDeploy(deployment.strategy.runOnce));
|
||||
continue;
|
||||
}
|
||||
|
||||
return pkg;
|
||||
const job = jobOrDeployment as Job;
|
||||
deps.push(...extractJob(job));
|
||||
}
|
||||
return deps;
|
||||
}
|
||||
|
||||
export function extractPackageFile(
|
||||
|
@ -142,31 +192,11 @@ export function extractPackageFile(
|
|||
}
|
||||
|
||||
for (const { jobs } of coerceArray(pkg.stages)) {
|
||||
for (const { steps } of coerceArray(jobs)) {
|
||||
for (const step of coerceArray(steps)) {
|
||||
const task = extractAzurePipelinesTasks(step.task);
|
||||
if (task) {
|
||||
deps.push(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
deps.push(...extractJobs(jobs));
|
||||
}
|
||||
|
||||
for (const { steps } of coerceArray(pkg.jobs)) {
|
||||
for (const step of coerceArray(steps)) {
|
||||
const task = extractAzurePipelinesTasks(step.task);
|
||||
if (task) {
|
||||
deps.push(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const step of coerceArray(pkg.steps)) {
|
||||
const task = extractAzurePipelinesTasks(step.task);
|
||||
if (task) {
|
||||
deps.push(task);
|
||||
}
|
||||
}
|
||||
deps.push(...extractJobs(pkg.jobs));
|
||||
deps.push(...extractSteps(pkg.steps));
|
||||
|
||||
if (!deps.length) {
|
||||
return null;
|
||||
|
|
81
lib/modules/manager/azure-pipelines/schema.ts
Normal file
81
lib/modules/manager/azure-pipelines/schema.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
import { z } from 'zod';
|
||||
import { LooseArray, Yaml } from '../../../util/schema-utils';
|
||||
|
||||
export const Step = z.object({
|
||||
task: z.string(),
|
||||
});
|
||||
export type Step = z.infer<typeof Step>;
|
||||
|
||||
export const Job = z.object({
|
||||
steps: LooseArray(Step),
|
||||
});
|
||||
export type Job = z.infer<typeof Job>;
|
||||
|
||||
export const Deploy = z
|
||||
.object({
|
||||
deploy: Job,
|
||||
preDeploy: Job,
|
||||
routeTraffic: Job,
|
||||
postRouteTraffic: Job,
|
||||
on: z
|
||||
.object({
|
||||
failure: Job,
|
||||
success: Job,
|
||||
})
|
||||
.partial(),
|
||||
})
|
||||
.partial();
|
||||
export type Deploy = z.infer<typeof Deploy>;
|
||||
|
||||
export const Deployment = z
|
||||
.object({
|
||||
strategy: z
|
||||
.object({
|
||||
runOnce: Deploy,
|
||||
rolling: Deploy,
|
||||
canary: Deploy,
|
||||
})
|
||||
.partial(),
|
||||
})
|
||||
.partial();
|
||||
export type Deployment = z.infer<typeof Deployment>;
|
||||
|
||||
export const Jobs = LooseArray(z.union([Job, Deployment]));
|
||||
export type Jobs = z.infer<typeof Jobs>;
|
||||
|
||||
export const Stage = z.object({
|
||||
jobs: Jobs,
|
||||
});
|
||||
export type Stage = z.infer<typeof Stage>;
|
||||
|
||||
export const Container = z.object({
|
||||
image: z.string(),
|
||||
});
|
||||
export type Container = z.infer<typeof Container>;
|
||||
|
||||
export const Repository = z.object({
|
||||
type: z.enum(['git', 'github', 'bitbucket']),
|
||||
name: z.string(),
|
||||
ref: z.string().optional(),
|
||||
});
|
||||
export type Repository = z.infer<typeof Repository>;
|
||||
|
||||
export const Resources = z
|
||||
.object({
|
||||
repositories: LooseArray(Repository),
|
||||
containers: LooseArray(Container),
|
||||
})
|
||||
.partial();
|
||||
export type Resources = z.infer<typeof Resources>;
|
||||
|
||||
export const AzurePipelines = z
|
||||
.object({
|
||||
resources: Resources,
|
||||
stages: LooseArray(Stage),
|
||||
jobs: Jobs,
|
||||
steps: LooseArray(Step),
|
||||
})
|
||||
.partial();
|
||||
export type AzurePipelines = z.infer<typeof AzurePipelines>;
|
||||
|
||||
export const AzurePipelinesYaml = Yaml.pipe(AzurePipelines);
|
|
@ -1,30 +0,0 @@
|
|||
export interface Container {
|
||||
image?: string | null;
|
||||
}
|
||||
export interface Repository {
|
||||
type: 'git' | 'github' | 'bitbucket';
|
||||
name: string;
|
||||
ref?: string | null;
|
||||
}
|
||||
export interface Resources {
|
||||
repositories?: Repository[];
|
||||
containers?: Container[];
|
||||
}
|
||||
export interface AzurePipelines {
|
||||
resources?: Resources;
|
||||
stages?: Stage[];
|
||||
jobs?: Job[];
|
||||
steps?: Step[];
|
||||
}
|
||||
|
||||
export interface Stage {
|
||||
jobs?: Job[];
|
||||
}
|
||||
|
||||
export interface Job {
|
||||
steps?: Step[];
|
||||
}
|
||||
|
||||
export interface Step {
|
||||
task: string;
|
||||
}
|
Loading…
Reference in a new issue