Merge pull request #22153 from R-Beck-2020/azure-devops-monorepo-support

Improve Azure Pipelines Mono Repo Support
This commit is contained in:
Patrik Oldsberg
2024-01-20 10:40:13 +01:00
committed by GitHub
9 changed files with 226 additions and 28 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-azure-devops': patch
---
Prefer `dev.azure.com/build-definition` annotation when it is provided, as it is more specific than `dev.azure.com/project-repo`. This can also be used as a filter for mono-repos.
+19
View File
@@ -61,6 +61,25 @@ spec:
# ...
```
#### Mono repos
If you have multiple entities within a single repo, you will need to specify which pipelines belong to each entity.
```yaml
dev.azure.com/project-repo: <my-project>/<my-repo>
dev.azure.com/build-definition: <build-definition-name>
```
#### Pipeline in different project to repo
If your pipeline is in a different project to the source code, you will need to specify this in the project annotation.
```yaml
dev.azure.com/project-repo: <project-with-source-code>/<my-repo>
dev.azure.com/build-definition: <build-definition-name>
dev.azure.com/project: <project-with-build-code>
```
#### Azure Pipelines Only
If you are only using Azure Pipelines along with a different SCM tool then you can use the following two annotations to see Builds:
@@ -99,7 +99,7 @@ describe('useBuildRuns', () => {
renderHook(() => useBuildRuns(entity), {
wrapper: Wrapper,
}),
).toThrow('Value for annotation "dev.azure.com/project" was not found');
).toThrow('Expected "dev.azure.com" annotations were not found');
});
it('should return throw when annotation invalid', async () => {
@@ -103,7 +103,7 @@ describe('useGitTags', () => {
renderHook(() => useGitTags(entity), {
wrapper: Wrapper,
}),
).toThrow('Value for annotation "dev.azure.com/project" was not found');
).toThrow('Expected "dev.azure.com" annotations were not found');
});
it('should return throw when annotation invalid', async () => {
@@ -102,7 +102,7 @@ describe('usePullRequests', () => {
renderHook(() => usePullRequests(entity), {
wrapper: Wrapper,
}),
).toThrow('Value for annotation "dev.azure.com/project" was not found');
).toThrow('Expected "dev.azure.com" annotations were not found');
});
it('should return throw when annotation invalid', async () => {
@@ -81,7 +81,7 @@ describe('useReadme', () => {
renderHook(() => useReadme(entity), {
wrapper: Wrapper,
}),
).toThrow('Value for annotation "dev.azure.com/project" was not found');
).toThrow('Expected "dev.azure.com" annotations were not found');
});
it('should return throw when annotation invalid', async () => {
+93 -1
View File
@@ -13,10 +13,102 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { azureDevOpsPlugin } from './plugin';
import { Entity } from '@backstage/catalog-model';
import { azureDevOpsPlugin, isAzurePipelinesAvailable } from './plugin';
describe('azure-devops', () => {
it('should export plugin', () => {
expect(azureDevOpsPlugin).toBeDefined();
});
describe('isAzurePipelinesAvailable', () => {
it('should be true when project-repo annotation is present', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'sample',
annotations: {
'dev.azure.com/project-repo': 'projectName/repoName',
},
},
};
expect(isAzurePipelinesAvailable(entity)).toBe(true);
});
it('should be true when project and build-definition annotation is present', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'sample',
annotations: {
'dev.azure.com/project': 'projectName',
'dev.azure.com/build-definition': 'buildDefinitionName',
},
},
};
expect(isAzurePipelinesAvailable(entity)).toBe(true);
});
it('should be true when project-repo and build-definition annotation is present', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'sample',
annotations: {
'dev.azure.com/project-repo': 'projectName/repoName',
'dev.azure.com/build-definition': 'buildDefinitionName',
},
},
};
expect(isAzurePipelinesAvailable(entity)).toBe(true);
});
it('should be false when no annotations are present', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'sample',
},
};
expect(isAzurePipelinesAvailable(entity)).toBe(false);
});
it('should be false when only project annotation is present', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'sample',
annotations: {
'dev.azure.com/project': 'projectName',
},
},
};
expect(isAzurePipelinesAvailable(entity)).toBe(false);
});
it('should be false when only build-definition annotation is present', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'sample',
annotations: {
'dev.azure.com/build-definition': 'buildDefinitionName',
},
},
};
expect(isAzurePipelinesAvailable(entity)).toBe(false);
});
});
});
@@ -18,6 +18,22 @@ import { Entity } from '@backstage/catalog-model';
import { getAnnotationValuesFromEntity } from './getAnnotationValuesFromEntity';
describe('getAnnotationValuesFromEntity', () => {
describe('without any annotations', () => {
it('should throw annotations not found', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
},
};
expect(() => getAnnotationValuesFromEntity(entity)).toThrow(
'Expected "dev.azure.com" annotations were not found',
);
});
});
describe('with valid project-repo annotation', () => {
it('should return project and repo', () => {
const entity: Entity = {
@@ -140,7 +156,7 @@ describe('getAnnotationValuesFromEntity', () => {
});
describe('with only project annotation', () => {
it('should should throw annotation not found error', () => {
it('should throw annotation not found error', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
@@ -163,7 +179,7 @@ describe('getAnnotationValuesFromEntity', () => {
});
describe('with only build-definition annotation', () => {
it('should should throw annotation not found error', () => {
it('should throw annotation not found error', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
@@ -381,4 +397,55 @@ describe('getAnnotationValuesFromEntity', () => {
);
});
});
describe('projectRepo and buildDefinition are provided', () => {
it('should return project, repo and buildDefinition', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
annotations: {
'dev.azure.com/build-definition': 'buildDefinitionName',
'dev.azure.com/project-repo': 'projectName/repoName',
},
},
};
const values = getAnnotationValuesFromEntity(entity);
expect(values).toEqual({
project: 'projectName',
repo: 'repoName',
definition: 'buildDefinitionName',
host: undefined,
org: undefined,
});
});
});
describe('project, projectRepo and buildDefinition are provided', () => {
it('should prefer project over project-repo.project and return no repo', () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
namespace: 'default',
name: 'project-repo',
annotations: {
'dev.azure.com/project': 'projectName',
'dev.azure.com/build-definition': 'buildDefinitionName',
'dev.azure.com/project-repo': 'ignoredProject/repoName',
},
},
};
const values = getAnnotationValuesFromEntity(entity);
expect(values).toEqual({
project: 'projectName',
repo: undefined,
definition: 'buildDefinitionName',
host: undefined,
org: undefined,
});
});
});
});
@@ -29,34 +29,49 @@ export function getAnnotationValuesFromEntity(entity: Entity): {
host?: string;
org?: string;
} {
const { host, org } = getHostOrg(entity.metadata.annotations);
const projectRepoValues = getProjectRepo(entity.metadata.annotations);
if (projectRepoValues.project && projectRepoValues.repo) {
return {
project: projectRepoValues.project,
repo: projectRepoValues.repo,
host,
org,
};
}
const hostOrg = getHostOrg(entity.metadata.annotations);
const projectRepo = getProjectRepo(entity.metadata.annotations);
const project =
entity.metadata.annotations?.[AZURE_DEVOPS_PROJECT_ANNOTATION];
if (!project) {
const definition =
entity.metadata.annotations?.[AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION];
if (definition) {
if (project) {
return {
project,
definition,
...hostOrg,
};
}
if (projectRepo.project) {
return {
project: projectRepo.project,
repo: projectRepo.repo,
definition,
...hostOrg,
};
}
throw new Error(
`Value for annotation "${AZURE_DEVOPS_PROJECT_ANNOTATION}" was not found`,
);
} else {
if (projectRepo.project) {
return {
project: projectRepo.project,
repo: projectRepo.repo,
...hostOrg,
};
}
if (project) {
throw new Error(
`Value for annotation "${AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION}" was not found`,
);
}
}
const definition =
entity.metadata.annotations?.[AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION];
if (!definition) {
throw new Error(
`Value for annotation "${AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION}" was not found`,
);
}
return { project, definition, host, org };
throw new Error('Expected "dev.azure.com" annotations were not found');
}
function getProjectRepo(annotations?: Record<string, string>): {