Merge branch 'master' into kp/fix-misleading-status-code-in-error-page
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
---
|
||||
'@backstage/plugin-explore-backend': patch
|
||||
---
|
||||
|
||||
Add support for the new backend system.
|
||||
|
||||
A new backend plugin for the explore backend
|
||||
was added and exported as `default`.
|
||||
|
||||
You can use it with the new backend system like
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-explore-backend'));
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/app-defaults': patch
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Change default icon for `kind:resource` to the storage icon.
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': minor
|
||||
---
|
||||
|
||||
**BREAKING**: The `app.routes.bindings` app-config mapping has been simplified. You now only need to specify the plugin ID and route ID on both sides of the mapping.
|
||||
|
||||
Old form:
|
||||
|
||||
```yaml
|
||||
app:
|
||||
routes:
|
||||
bindings:
|
||||
plugin.catalog.externalRoutes.viewTechDoc: plugin.techdocs.routes.docRoot
|
||||
plugin.catalog.externalRoutes.createComponent: plugin.catalog-import.routes.importPage
|
||||
```
|
||||
|
||||
New form:
|
||||
|
||||
```yaml
|
||||
app:
|
||||
routes:
|
||||
bindings:
|
||||
catalog.viewTechDoc: techdocs.docRoot
|
||||
catalog.createComponent: catalog-import.importPage
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': patch
|
||||
---
|
||||
|
||||
Improved the error message when data input/output shapes do not match
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Updated dependencies in frontend plugin templates
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
'@backstage/plugin-cloudbuild': minor
|
||||
---
|
||||
|
||||
Changed build list view to automatically filter builds based on repository name matching component-info's metadata.name.
|
||||
Added optional `google.com/cloudbuild-repo-name` annotation which allows you to specify a different repository to filter on.
|
||||
Added optional `google.com/cloudbuild-trigger-name` annotation which allows you to filter based on a trigger name instead of a repo name.
|
||||
Updated the ReadMe with information about the filtering and some other minor verbiage updates.
|
||||
Changed `substitutions.BRANCH_NAME` to `substitutions.REF_NAME` so that the Ref field is populated properly.
|
||||
Added optional `google.com/cloudbuild-location` annotation which allows you to specify the Cloud Build location of your builds. Default is global scope.
|
||||
Changed build list view to show builds in a specific location if the location annotation is used.
|
||||
Updated ReadMe with information about the use of the location filtering.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Updated the backend module template to make the module instance the package default export.
|
||||
@@ -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.
|
||||
@@ -1 +1 @@
|
||||
This terraform file should create a S3 bucket and setup IAM with a user with an inline policy which gives the user access to the bucket. After you have created the bucket, user and policy you should go to the user in the AWS console and create an access key. This access key should be used as the env variables in step 3a [here](https://backstage.io/docs/features/techdocs/using-cloud-storage#configuring-aws-s3-bucket-with-techdocs).
|
||||
This Terraform file should create an S3 bucket and set up IAM with a user and an inline policy that authorises access to the bucket. After you've set up the bucket, user, and policy, go to the user's AWS dashboard and create an access key. This access key should be used to configure the environment variables in step 3a [here](https://backstage.io/docs/features/techdocs/using-cloud-storage#configuring-aws-s3-bucket-with-techdocs).
|
||||
|
||||
@@ -241,10 +241,10 @@ app:
|
||||
bindings:
|
||||
# point to the Scaffolder create component page when the Catalog create component ref is used
|
||||
# highlight-next-line
|
||||
plugin.catalog.externalRoutes.createComponent: plugin.scaffolder.routes.index
|
||||
catalog.createComponent: scaffolder.index
|
||||
# point to the Catalog details page when the Scaffolder component details ref is used
|
||||
# highlight-next-line
|
||||
plugin.scaffolder.externalRoutes.componentDetails: plugin.catalog.routes.details
|
||||
scaffolder.componentDetails: catalog.details
|
||||
```
|
||||
|
||||
We also have the ability to express this in code as an option to `createApp`, but you of course only need to use one of these two methods:
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
title: AWS App Developer Tools
|
||||
title: OPA on AWS
|
||||
author: Amazon Web Services
|
||||
authorUrl: https://aws.amazon.com/
|
||||
category: Infrastructure
|
||||
description: Create and manage AWS Apps within Backstage
|
||||
documentation: https://github.com/awslabs/app-development-for-backstage-io-on-aws#readme
|
||||
iconUrl: https://github.com/awslabs/app-development-for-backstage-io-on-aws/blob/main/docs/images/AWS_logo.png?raw=true
|
||||
description: Orchestrate Platforms and Applications (OPA) allows customers to build and manage AWS Apps & Environments within Backstage
|
||||
documentation: https://opaonaws.io
|
||||
iconUrl: https://github.com/awslabs/app-development-for-backstage-io-on-aws/blob/main/website/static/img/white_OPA_text02.png?raw=true
|
||||
npmPackageName: '@aws/plugin-aws-apps-for-backstage'
|
||||
addedDate: '2023-05-10'
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: Backchat GenAI
|
||||
author: benwilcock
|
||||
authorUrl: https://github.com/benwilcock
|
||||
category: Services
|
||||
description: Access your favorite open source GenAI GUIs privately from Backstage. Chat wth large language models in your portal. Choose from hundreds of LLMs. Run inferencing wherever you like - local or remote, CPU or GPU - it's up to you!
|
||||
documentation: https://github.com/benwilcock/backstage-plugin-backchat
|
||||
iconUrl: /img/backchat-logo.png
|
||||
npmPackageName: '@benbravo73/backstage-plugin-backchat'
|
||||
addedDate: '2024-01-12'
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: End of life
|
||||
author: dweber019
|
||||
authorUrl: https://github.com/dweber019
|
||||
category: Quality
|
||||
description: Display end of life data for entities from endoflife.data
|
||||
documentation: https://github.com/dweber019/backstage-plugin-endoflife
|
||||
iconUrl: https://raw.githubusercontent.com/dweber019/backstage-plugin-endoflife/main/plugins/endoflife/docs/pluginIcon.png
|
||||
npmPackageName: '@dweber019/backstage-plugin-endoflife'
|
||||
addedDate: '2024-01-18'
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: GitHub Codespaces
|
||||
author: Aditya Singhal
|
||||
authorUrl: https://github.com/adityasinghal26
|
||||
category: Development
|
||||
description: Integrates GitHub Codespaces for a Backstage component with the Authenticated User.
|
||||
documentation: https://github.com/adityasinghal26/backstage-plugins/tree/main/plugins/github-codespaces
|
||||
iconUrl: https://github.com/adityasinghal26/backstage-plugins/blob/00c9c00ba9acc3135014d6454ccf04f573195eef/plugins/github-codespaces/images/GitHubLogo.png
|
||||
npmPackageName: '@adityasinghal26/plugin-github-codespaces'
|
||||
tags:
|
||||
- github
|
||||
- codespaces
|
||||
- development
|
||||
- devcontainers
|
||||
addedDate: '2023-12-30'
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 232 KiB |
@@ -34,7 +34,7 @@ import MuiMenuBookIcon from '@material-ui/icons/MenuBook';
|
||||
import MuiPeopleIcon from '@material-ui/icons/People';
|
||||
import MuiPersonIcon from '@material-ui/icons/Person';
|
||||
import MuiWarningIcon from '@material-ui/icons/Warning';
|
||||
import MuiWorkIcon from '@material-ui/icons/Work';
|
||||
import MuiStorageIcon from '@material-ui/icons/Storage';
|
||||
import MuiFeaturedPlayListIcon from '@material-ui/icons/FeaturedPlayList';
|
||||
|
||||
export const icons = {
|
||||
@@ -58,7 +58,7 @@ export const icons = {
|
||||
'kind:location': MuiLocationOnIcon as IconComponent,
|
||||
'kind:system': MuiCategoryIcon as IconComponent,
|
||||
'kind:user': MuiPersonIcon as IconComponent,
|
||||
'kind:resource': MuiWorkIcon as IconComponent,
|
||||
'kind:resource': MuiStorageIcon as IconComponent,
|
||||
'kind:template': MuiFeaturedPlayListIcon as IconComponent,
|
||||
user: MuiPersonIcon as IconComponent,
|
||||
warning: MuiWarningIcon as IconComponent,
|
||||
|
||||
@@ -3,9 +3,9 @@ app:
|
||||
packages: 'all' # ✨
|
||||
routes:
|
||||
bindings:
|
||||
plugin.pages.externalRoutes.pageX: plugin.pages.routes.pageX
|
||||
plugin.catalog.externalRoutes.viewTechDoc: plugin.techdocs.routes.docRoot
|
||||
plugin.catalog.externalRoutes.createComponent: plugin.catalog-import.routes.importPage
|
||||
pages.pageX: pages.pageX
|
||||
catalog.viewTechDoc: techdocs.docRoot
|
||||
catalog.createComponent: catalog-import.importPage
|
||||
|
||||
extensions:
|
||||
# - apis.plugin.graphiql.browse.gitlab: true
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export { {{moduleVar}} } from './module';
|
||||
export { {{moduleVar}} as default } from './module';
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
"@backstage/core-app-api": "{{versionQuery '@backstage/core-app-api'}}",
|
||||
"@backstage/dev-utils": "{{versionQuery '@backstage/dev-utils'}}",
|
||||
"@backstage/test-utils": "{{versionQuery '@backstage/test-utils'}}",
|
||||
"@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '5.10.1'}}",
|
||||
"@testing-library/react": "{{versionQuery '@testing-library/react' '12.1.3'}}",
|
||||
"@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '6.0.0'}}",
|
||||
"@testing-library/react": "{{versionQuery '@testing-library/react' '14.0.0'}}",
|
||||
"@testing-library/user-event": "{{versionQuery '@testing-library/user-event' '14.0.0'}}",
|
||||
"msw": "{{versionQuery 'msw' '1.0.0'}}"
|
||||
},
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "{{versionQuery '@backstage/cli'}}",
|
||||
"@backstage/test-utils": "{{versionQuery '@backstage/test-utils'}}",
|
||||
"@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '5.10.1'}}",
|
||||
"@testing-library/react": "{{versionQuery '@testing-library/react' '12.1.3'}}"
|
||||
"@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '6.0.0'}}",
|
||||
"@testing-library/react": "{{versionQuery '@testing-library/react' '14.0.0'}}"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "{{versionQuery '@backstage/cli'}}",
|
||||
"@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '5.10.1'}}"
|
||||
"@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '6.0.0'}}"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -37,15 +37,13 @@ describe('collectRouteIds', () => {
|
||||
createPlugin({ id: 'test', routes: { ref }, externalRoutes: { extRef } }),
|
||||
]);
|
||||
expect(Object.fromEntries(collected.routes)).toEqual({
|
||||
'plugin.test.routes.ref': ref,
|
||||
'test.ref': ref,
|
||||
});
|
||||
expect(Object.fromEntries(collected.externalRoutes)).toEqual({
|
||||
'plugin.test.externalRoutes.extRef': extRef,
|
||||
'test.extRef': extRef,
|
||||
});
|
||||
|
||||
expect(String(ref)).toBe('RouteRef{plugin.test.routes.ref}');
|
||||
expect(String(extRef)).toBe(
|
||||
'ExternalRouteRef{plugin.test.externalRoutes.extRef}',
|
||||
);
|
||||
expect(String(ref)).toBe('RouteRef{test.ref}');
|
||||
expect(String(extRef)).toBe('ExternalRouteRef{test.extRef}');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ export function collectRouteIds(features: FrontendFeature[]): RouteRefsById {
|
||||
}
|
||||
|
||||
for (const [name, ref] of Object.entries(feature.routes)) {
|
||||
const refId = `plugin.${feature.id}.routes.${name}`;
|
||||
const refId = `${feature.id}.${name}`;
|
||||
if (routesById.has(refId)) {
|
||||
throw new Error(`Unexpected duplicate route '${refId}'`);
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export function collectRouteIds(features: FrontendFeature[]): RouteRefsById {
|
||||
}
|
||||
}
|
||||
for (const [name, ref] of Object.entries(feature.externalRoutes)) {
|
||||
const refId = `plugin.${feature.id}.externalRoutes.${name}`;
|
||||
const refId = `${feature.id}.${name}`;
|
||||
if (externalRoutesById.has(refId)) {
|
||||
throw new Error(`Unexpected duplicate external route '${refId}'`);
|
||||
}
|
||||
|
||||
@@ -615,8 +615,8 @@ describe('createAppNodeInstance', () => {
|
||||
),
|
||||
),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'app/test', input 'singleton' did not receive required extension data 'other' from extension 'app/test'",
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Failed to instantiate extension 'app/test', extension 'app/test' could not be attached because its output data ('test', 'other') does not match what the input 'singleton' requires ('other')"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,8 +37,17 @@ function resolveInputData(
|
||||
return mapValues(dataMap, ref => {
|
||||
const value = attachment.instance?.getData(ref);
|
||||
if (value === undefined && !ref.config.optional) {
|
||||
const expected = Object.values(dataMap)
|
||||
.filter(r => !r.config.optional)
|
||||
.map(r => `'${r.id}'`)
|
||||
.join(', ');
|
||||
|
||||
const provided = [...(attachment.instance?.getDataRefs() ?? [])]
|
||||
.map(r => `'${r.id}'`)
|
||||
.join(', ');
|
||||
|
||||
throw new Error(
|
||||
`input '${inputName}' did not receive required extension data '${ref.id}' from extension '${attachment.spec.id}'`,
|
||||
`extension '${attachment.spec.id}' could not be attached because its output data (${provided}) does not match what the input '${inputName}' requires (${expected})`,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
|
||||
@@ -63,8 +63,8 @@ describe('createExtensionTester', () => {
|
||||
factory: () => ({ path: '/foo' }),
|
||||
});
|
||||
const tester = createExtensionTester(extension);
|
||||
expect(() => tester.render()).toThrow(
|
||||
"Failed to instantiate extension 'app/routes', input 'routes' did not receive required extension data 'core.reactElement' from extension 'test'",
|
||||
expect(() => tester.render()).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Failed to instantiate extension 'app/routes', extension 'test' could not be attached because its output data ('core.routing.path') does not match what the input 'routes' requires ('core.routing.path', 'core.reactElement')"`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>): {
|
||||
|
||||
@@ -26,7 +26,7 @@ import LocationOnIcon from '@material-ui/icons/LocationOn';
|
||||
import MemoryIcon from '@material-ui/icons/Memory';
|
||||
import PeopleIcon from '@material-ui/icons/People';
|
||||
import PersonIcon from '@material-ui/icons/Person';
|
||||
import WorkIcon from '@material-ui/icons/Work';
|
||||
import StorageIcon from '@material-ui/icons/Storage';
|
||||
import { DefaultEntityPresentationApiRenderer } from './DefaultEntityPresentationApi';
|
||||
|
||||
export const DEFAULT_CACHE_TTL: HumanDuration = { seconds: 10 };
|
||||
@@ -39,7 +39,7 @@ export const DEFAULT_ICONS: Record<string, IconComponent> = {
|
||||
api: ExtensionIcon,
|
||||
component: MemoryIcon,
|
||||
system: CategoryIcon,
|
||||
resource: WorkIcon,
|
||||
resource: StorageIcon,
|
||||
domain: ApartmentIcon,
|
||||
location: LocationOnIcon,
|
||||
user: PersonIcon,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Google Cloud Build
|
||||
# Google Cloud Build Plugin
|
||||
|
||||
### Welcome to the Google Cloud Build plugin!
|
||||
|
||||
@@ -47,7 +47,7 @@ const cicdContent = (
|
||||
|
||||
##### OPTIONAL
|
||||
|
||||
If you don't use GitHub Actions, or don't want to show it on your CI/CD page, then you can remove the switch case for it
|
||||
If you don't use GitHub Actions, or don't want to show it on your CI/CD page, then you can remove the switch case for it:
|
||||
|
||||
```diff
|
||||
// packages/app/src/components/catalog/EntityPage.tsx
|
||||
@@ -62,9 +62,9 @@ const cicdContent = (
|
||||
- </EntitySwitch.Case>
|
||||
```
|
||||
|
||||
### Add annotation to your component-info.yaml file.
|
||||
### Add annotation(s) to your component-info.yaml file
|
||||
|
||||
Any component, that you would like the Cloud Build Plugin to populate for, should include the following annotation:
|
||||
Any component, that you would like the Cloud Build Plugin to populate for, should include the following `cloudbuild-project-slug` annotation. This annotation sets the GCP project name to be used for pulling the Cloud Build details from.
|
||||
|
||||
```diff
|
||||
// component-info.yaml
|
||||
@@ -79,3 +79,62 @@ spec:
|
||||
type: website
|
||||
lifecycle: development
|
||||
```
|
||||
|
||||
By default, the cloud build results list is filtered by repository name equal to the name you have set in your component-info.yaml file. This is `metadata.name`. So if your metadata.name is `backstage` then it will only show builds matching the backstage repo name.
|
||||
|
||||
Additionally, build results are pulled from the `global` region by default.
|
||||
|
||||
#### Change Filtering
|
||||
|
||||
If you need the page to be filtered on a different repository name, then you can use the following annotation:
|
||||
|
||||
```diff
|
||||
// component-info.yaml
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: backstage
|
||||
description: Backstage application.
|
||||
annotations:
|
||||
google.com/cloudbuild-project-slug: your-project-name
|
||||
+ google.com/cloudbuild-repo-name: my-backstage
|
||||
spec:
|
||||
type: website
|
||||
lifecycle: development
|
||||
```
|
||||
|
||||
You can also automatically filter the results based on trigger name instead of repository name. To do so, use the following annotation:
|
||||
|
||||
```diff
|
||||
// component-info.yaml
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: backstage
|
||||
description: Backstage application.
|
||||
annotations:
|
||||
google.com/cloudbuild-project-slug: your-project-name
|
||||
+ google.com/cloudbuild-trigger-name: my-trigger-name
|
||||
spec:
|
||||
type: website
|
||||
lifecycle: development
|
||||
```
|
||||
|
||||
`Note:` The `cloudbuild-repo-name` annotation takes precedence over the `cloudbuild-trigger-name` annotation. So if you happen to use both annotations, cloudbuild-repo-name will be used. It is recommended to use one or the other if required.
|
||||
|
||||
If you need to pull Cloud Build results from a location or region other than the global scope, then use the following annotation:
|
||||
|
||||
```diff
|
||||
// component-info.yaml
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: backstage
|
||||
description: Backstage application.
|
||||
annotations:
|
||||
google.com/cloudbuild-project-slug: your-project-name
|
||||
+ google.com/cloudbuild-location: us-central1
|
||||
spec:
|
||||
type: website
|
||||
lifecycle: development
|
||||
```
|
||||
|
||||
@@ -58,17 +58,22 @@ export const CLOUDBUILD_ANNOTATION = 'google.com/cloudbuild-project-slug';
|
||||
export type CloudbuildApi = {
|
||||
listWorkflowRuns: (options: {
|
||||
projectId: string;
|
||||
location: string;
|
||||
cloudBuildFilter: string;
|
||||
}) => Promise<ActionsListWorkflowRunsForRepoResponseData>;
|
||||
getWorkflow: (options: {
|
||||
projectId: string;
|
||||
location: string;
|
||||
id: string;
|
||||
}) => Promise<ActionsGetWorkflowResponseData>;
|
||||
getWorkflowRun: (options: {
|
||||
projectId: string;
|
||||
location: string;
|
||||
id: string;
|
||||
}) => Promise<ActionsGetWorkflowResponseData>;
|
||||
reRunWorkflow: (options: {
|
||||
projectId: string;
|
||||
location: string;
|
||||
runId: string;
|
||||
}) => Promise<any>;
|
||||
};
|
||||
@@ -84,19 +89,27 @@ export class CloudbuildClient implements CloudbuildApi {
|
||||
// (undocumented)
|
||||
getWorkflow(options: {
|
||||
projectId: string;
|
||||
location: string;
|
||||
id: string;
|
||||
}): Promise<ActionsGetWorkflowResponseData>;
|
||||
// (undocumented)
|
||||
getWorkflowRun(options: {
|
||||
projectId: string;
|
||||
location: string;
|
||||
id: string;
|
||||
}): Promise<ActionsGetWorkflowResponseData>;
|
||||
// (undocumented)
|
||||
listWorkflowRuns(options: {
|
||||
projectId: string;
|
||||
location: string;
|
||||
cloudBuildFilter: string;
|
||||
}): Promise<ActionsListWorkflowRunsForRepoResponseData>;
|
||||
// (undocumented)
|
||||
reRunWorkflow(options: { projectId: string; runId: string }): Promise<void>;
|
||||
reRunWorkflow(options: {
|
||||
projectId: string;
|
||||
location: string;
|
||||
runId: string;
|
||||
}): Promise<void>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -234,11 +247,11 @@ export interface StorageSource {
|
||||
|
||||
// @public (undocumented)
|
||||
export interface Substitutions {
|
||||
// (undocumented)
|
||||
BRANCH_NAME: string;
|
||||
// (undocumented)
|
||||
COMMIT_SHA: string;
|
||||
// (undocumented)
|
||||
REF_NAME: string;
|
||||
// (undocumented)
|
||||
REPO_NAME: string;
|
||||
// (undocumented)
|
||||
REVISION_ID: string;
|
||||
|
||||
@@ -29,17 +29,22 @@ export const cloudbuildApiRef = createApiRef<CloudbuildApi>({
|
||||
export type CloudbuildApi = {
|
||||
listWorkflowRuns: (options: {
|
||||
projectId: string;
|
||||
location: string;
|
||||
cloudBuildFilter: string;
|
||||
}) => Promise<ActionsListWorkflowRunsForRepoResponseData>;
|
||||
getWorkflow: (options: {
|
||||
projectId: string;
|
||||
location: string;
|
||||
id: string;
|
||||
}) => Promise<ActionsGetWorkflowResponseData>;
|
||||
getWorkflowRun: (options: {
|
||||
projectId: string;
|
||||
location: string;
|
||||
id: string;
|
||||
}) => Promise<ActionsGetWorkflowResponseData>;
|
||||
reRunWorkflow: (options: {
|
||||
projectId: string;
|
||||
location: string;
|
||||
runId: string;
|
||||
}) => Promise<any>;
|
||||
};
|
||||
|
||||
@@ -27,11 +27,14 @@ export class CloudbuildClient implements CloudbuildApi {
|
||||
|
||||
async reRunWorkflow(options: {
|
||||
projectId: string;
|
||||
location: string;
|
||||
runId: string;
|
||||
}): Promise<void> {
|
||||
await fetch(
|
||||
`https://cloudbuild.googleapis.com/v1/projects/${encodeURIComponent(
|
||||
options.projectId,
|
||||
)}/locations/${encodeURIComponent(
|
||||
options.location,
|
||||
)}/builds/${encodeURIComponent(options.runId)}:retry`,
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -45,11 +48,15 @@ export class CloudbuildClient implements CloudbuildApi {
|
||||
|
||||
async listWorkflowRuns(options: {
|
||||
projectId: string;
|
||||
location: string;
|
||||
cloudBuildFilter: string;
|
||||
}): Promise<ActionsListWorkflowRunsForRepoResponseData> {
|
||||
const workflowRuns = await fetch(
|
||||
`https://cloudbuild.googleapis.com/v1/projects/${encodeURIComponent(
|
||||
options.projectId,
|
||||
)}/builds`,
|
||||
)}/locations/${encodeURIComponent(
|
||||
options.location,
|
||||
)}/builds?filter=${encodeURIComponent(options.cloudBuildFilter)}`,
|
||||
{
|
||||
headers: new Headers({
|
||||
Accept: '*/*',
|
||||
@@ -66,11 +73,14 @@ export class CloudbuildClient implements CloudbuildApi {
|
||||
|
||||
async getWorkflow(options: {
|
||||
projectId: string;
|
||||
location: string;
|
||||
id: string;
|
||||
}): Promise<ActionsGetWorkflowResponseData> {
|
||||
const workflow = await fetch(
|
||||
`https://cloudbuild.googleapis.com/v1/projects/${encodeURIComponent(
|
||||
options.projectId,
|
||||
)}/locations/${encodeURIComponent(
|
||||
options.location,
|
||||
)}/builds/${encodeURIComponent(options.id)}`,
|
||||
{
|
||||
headers: new Headers({
|
||||
@@ -87,11 +97,14 @@ export class CloudbuildClient implements CloudbuildApi {
|
||||
|
||||
async getWorkflowRun(options: {
|
||||
projectId: string;
|
||||
location: string;
|
||||
id: string;
|
||||
}): Promise<ActionsGetWorkflowResponseData> {
|
||||
const workflow = await fetch(
|
||||
`https://cloudbuild.googleapis.com/v1/projects/${encodeURIComponent(
|
||||
options.projectId,
|
||||
)}/locations/${encodeURIComponent(
|
||||
options.location,
|
||||
)}/builds/${encodeURIComponent(options.id)}`,
|
||||
{
|
||||
headers: new Headers({
|
||||
|
||||
@@ -81,7 +81,7 @@ export interface Options {
|
||||
export interface Substitutions {
|
||||
COMMIT_SHA: string;
|
||||
SHORT_SHA: string;
|
||||
BRANCH_NAME: string;
|
||||
REF_NAME: string;
|
||||
REPO_NAME: string;
|
||||
REVISION_ID: string;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import { WorkflowRunStatus } from '../WorkflowRunStatus';
|
||||
import { makeStyles, LinearProgress } from '@material-ui/core';
|
||||
import ExternalLinkIcon from '@material-ui/icons/Launch';
|
||||
import { CLOUDBUILD_ANNOTATION } from '../useProjectName';
|
||||
import { getLocation } from '../useLocation';
|
||||
import { getCloudbuildFilter } from '../useCloudBuildFilter';
|
||||
|
||||
import {
|
||||
InfoCard,
|
||||
@@ -79,9 +81,13 @@ export const LatestWorkflowRunCard = (props: { branch: string }) => {
|
||||
const { entity } = useEntity();
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const projectId = entity?.metadata.annotations?.[CLOUDBUILD_ANNOTATION] || '';
|
||||
const location = getLocation(entity);
|
||||
const cloudBuildFilter = getCloudbuildFilter(entity);
|
||||
|
||||
const [{ runs, loading, error }] = useWorkflowRuns({
|
||||
projectId,
|
||||
location,
|
||||
cloudBuildFilter,
|
||||
});
|
||||
const lastRun = runs?.[0] ?? ({} as WorkflowRun);
|
||||
useEffect(() => {
|
||||
|
||||
@@ -34,6 +34,7 @@ import { useProjectName } from '../useProjectName';
|
||||
import { WorkflowRunStatus } from '../WorkflowRunStatus';
|
||||
import { useWorkflowRunsDetails } from './useWorkflowRunsDetails';
|
||||
import { Breadcrumbs, Link, WarningPanel } from '@backstage/core-components';
|
||||
import { getLocation } from '../useLocation';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
@@ -63,8 +64,9 @@ const useStyles = makeStyles(theme => ({
|
||||
export const WorkflowRunDetails = (props: { entity: Entity }) => {
|
||||
const { value: projectName, loading, error } = useProjectName(props.entity);
|
||||
const [projectId] = (projectName ?? '/').split('/');
|
||||
const location = getLocation(props.entity);
|
||||
|
||||
const details = useWorkflowRunsDetails(projectId);
|
||||
const details = useWorkflowRunsDetails(projectId, location);
|
||||
|
||||
const classes = useStyles();
|
||||
if (error) {
|
||||
@@ -96,9 +98,9 @@ export const WorkflowRunDetails = (props: { entity: Entity }) => {
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Branch</Typography>
|
||||
<Typography noWrap>Ref</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details.value?.substitutions.BRANCH_NAME}</TableCell>
|
||||
<TableCell>{details.value?.substitutions.REF_NAME}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
|
||||
@@ -18,13 +18,14 @@ import { cloudbuildApiRef } from '../../api';
|
||||
import { useApi, useRouteRefParams } from '@backstage/core-plugin-api';
|
||||
import { buildRouteRef } from '../../routes';
|
||||
|
||||
export const useWorkflowRunsDetails = (projectId: string) => {
|
||||
export const useWorkflowRunsDetails = (projectId: string, location: string) => {
|
||||
const api = useApi(cloudbuildApiRef);
|
||||
const { id } = useRouteRefParams(buildRouteRef);
|
||||
const details = useAsync(async () => {
|
||||
return projectId
|
||||
? api.getWorkflowRun({
|
||||
projectId,
|
||||
location,
|
||||
id: id,
|
||||
})
|
||||
: Promise.reject('No projectId provided');
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('<WorkflowRunsTableView />', () => {
|
||||
substitutions: {
|
||||
COMMIT_SHA: 'e3adasd2e3adasd2e3adasd2',
|
||||
SHORT_SHA: 'f12j1231',
|
||||
BRANCH_NAME: 'main',
|
||||
REF_NAME: 'main',
|
||||
REPO_NAME: 'backstage',
|
||||
REVISION_ID: 'g123123',
|
||||
},
|
||||
|
||||
@@ -26,6 +26,8 @@ import { buildRouteRef } from '../../routes';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Table, TableColumn, Link } from '@backstage/core-components';
|
||||
import { useRouteRef } from '@backstage/core-plugin-api';
|
||||
import { getLocation } from '../useLocation';
|
||||
import { getCloudbuildFilter } from '../useCloudBuildFilter';
|
||||
|
||||
const generatedColumns: TableColumn[] = [
|
||||
{
|
||||
@@ -71,7 +73,7 @@ const generatedColumns: TableColumn[] = [
|
||||
title: 'Ref',
|
||||
render: (row: Partial<WorkflowRun>) => (
|
||||
<Typography variant="body2" noWrap>
|
||||
{row.substitutions?.BRANCH_NAME}
|
||||
{row.substitutions?.REF_NAME}
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
@@ -162,9 +164,12 @@ export const WorkflowRunsTableView = ({
|
||||
export const WorkflowRunsTable = (props: { entity: Entity }) => {
|
||||
const { value: projectName, loading } = useProjectName(props.entity);
|
||||
const [projectId] = (projectName ?? '/').split('/');
|
||||
|
||||
const location = getLocation(props.entity);
|
||||
const cloudBuildFilter = getCloudbuildFilter(props.entity);
|
||||
const [tableProps, { retry, setPage, setPageSize }] = useWorkflowRuns({
|
||||
projectId,
|
||||
location,
|
||||
cloudBuildFilter,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
const CLOUDBUILD_FILTER_REPO_STRING = 'substitutions.REPO_NAME=';
|
||||
const CLOUDBUILD_FILTER_REPO_ANNOTATION = 'google.com/cloudbuild-repo-name';
|
||||
const CLOUDBUILD_FILTER_TRIGGER_STRING = 'substitutions.TRIGGER_NAME=';
|
||||
const CLOUDBUILD_FILTER_TRIGGER_ANNOTATION =
|
||||
'google.com/cloudbuild-trigger-name';
|
||||
|
||||
/** @public */
|
||||
|
||||
export const getCloudbuildFilter = (entity: Entity) => {
|
||||
const repoAnnotation =
|
||||
entity?.metadata.annotations?.[CLOUDBUILD_FILTER_REPO_ANNOTATION] ?? '';
|
||||
const triggerAnnotation =
|
||||
entity?.metadata.annotations?.[CLOUDBUILD_FILTER_TRIGGER_ANNOTATION] ?? '';
|
||||
if (repoAnnotation) {
|
||||
const cloudbuildFilter = CLOUDBUILD_FILTER_REPO_STRING + repoAnnotation;
|
||||
return cloudbuildFilter;
|
||||
} else if (triggerAnnotation) {
|
||||
const cloudbuildFilter =
|
||||
CLOUDBUILD_FILTER_TRIGGER_STRING + triggerAnnotation;
|
||||
return cloudbuildFilter;
|
||||
}
|
||||
const entityName = entity?.metadata.name ?? '';
|
||||
const cloudbuildFilter = CLOUDBUILD_FILTER_REPO_STRING + entityName;
|
||||
return cloudbuildFilter;
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
const CLOUDBUILD_LOCATION_ANNOTATION = 'google.com/cloudbuild-location';
|
||||
|
||||
/** @public */
|
||||
|
||||
export const getLocation = (entity: Entity) => {
|
||||
const locationAnnotation =
|
||||
entity?.metadata.annotations?.[CLOUDBUILD_LOCATION_ANNOTATION] ?? '';
|
||||
if (locationAnnotation) {
|
||||
return locationAnnotation;
|
||||
}
|
||||
return 'global';
|
||||
};
|
||||
@@ -33,8 +33,12 @@ export type WorkflowRun = {
|
||||
rerun: () => void;
|
||||
};
|
||||
|
||||
export function useWorkflowRuns(options: { projectId: string }) {
|
||||
const { projectId } = options;
|
||||
export function useWorkflowRuns(options: {
|
||||
projectId: string;
|
||||
location: string;
|
||||
cloudBuildFilter: string;
|
||||
}) {
|
||||
const { projectId, location, cloudBuildFilter } = options;
|
||||
const api = useApi(cloudbuildApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
@@ -51,6 +55,8 @@ export function useWorkflowRuns(options: { projectId: string }) {
|
||||
return api
|
||||
.listWorkflowRuns({
|
||||
projectId,
|
||||
location,
|
||||
cloudBuildFilter,
|
||||
})
|
||||
.then(
|
||||
(
|
||||
@@ -65,6 +71,7 @@ export function useWorkflowRuns(options: { projectId: string }) {
|
||||
try {
|
||||
await api.reRunWorkflow({
|
||||
projectId,
|
||||
location,
|
||||
runId: run.id,
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -9,6 +9,36 @@ for these tools.
|
||||
|
||||
### Adding the plugin to your `packages/backend`
|
||||
|
||||
Install dependencies
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
yarn add --cwd packages/backend @backstage/plugin-explore-backend
|
||||
```
|
||||
|
||||
Add feature
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
backend.add(import('@backstage/plugin-explore-backend'));
|
||||
```
|
||||
|
||||
Config:
|
||||
|
||||
```yaml
|
||||
explore:
|
||||
tools:
|
||||
- title: New Relic
|
||||
description: new relic plugin
|
||||
url: /newrelic
|
||||
image: https://i.imgur.com/L37ikrX.jpg
|
||||
tags:
|
||||
- newrelic
|
||||
- proxy
|
||||
- nerdGraph
|
||||
```
|
||||
|
||||
### Adding the plugin to your `packages/backend` (old)
|
||||
|
||||
#### Tools as Config
|
||||
|
||||
Install dependencies
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
import { Config } from '@backstage/config';
|
||||
import { ExploreTool } from '@backstage/plugin-explore-common';
|
||||
import express from 'express';
|
||||
@@ -16,6 +17,10 @@ import type { ToolDocumentCollatorFactoryOptions as ToolDocumentCollatorFactoryO
|
||||
// @public (undocumented)
|
||||
export function createRouter(options: RouterOptions): Promise<express.Router>;
|
||||
|
||||
// @public
|
||||
const explorePlugin: () => BackendFeature;
|
||||
export default explorePlugin;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ExploreToolProvider {
|
||||
getTools(request: GetExploreToolsRequest): Promise<GetExploreToolsResponse>;
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "workspace:^",
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/plugin-explore-common": "workspace:^",
|
||||
"@backstage/plugin-search-backend-module-explore": "workspace:^",
|
||||
@@ -42,6 +43,7 @@
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"supertest": "^6.2.4"
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export { explorePlugin as default } from './plugin';
|
||||
export * from './service';
|
||||
export * from './tools';
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
|
||||
import { ExploreTool } from '@backstage/plugin-explore-common';
|
||||
import { explorePlugin } from './plugin';
|
||||
|
||||
describe('explorePlugin', () => {
|
||||
it('should register explore plugin and its router', async () => {
|
||||
const tool: ExploreTool = {
|
||||
title: 'Tool Title',
|
||||
image: 'https://example.com/image.png',
|
||||
url: 'https://example.com',
|
||||
lifecycle: 'production',
|
||||
tags: ['tag1', 'tag2'],
|
||||
};
|
||||
|
||||
const httpRouterMock = mockServices.httpRouter.mock();
|
||||
|
||||
await startTestBackend({
|
||||
extensionPoints: [],
|
||||
features: [
|
||||
explorePlugin(),
|
||||
httpRouterMock.factory,
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
explore: {
|
||||
tools: [tool],
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(httpRouterMock.use).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { loggerToWinstonLogger } from '@backstage/backend-common';
|
||||
import {
|
||||
coreServices,
|
||||
createBackendPlugin,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { createRouter } from './service';
|
||||
import { StaticExploreToolProvider } from './tools';
|
||||
|
||||
/**
|
||||
* The explore backend plugin.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const explorePlugin = createBackendPlugin({
|
||||
pluginId: 'explore',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
config: coreServices.rootConfig,
|
||||
httpRouter: coreServices.httpRouter,
|
||||
logger: coreServices.logger,
|
||||
},
|
||||
async init({ config, httpRouter, logger }) {
|
||||
httpRouter.use(
|
||||
await createRouter({
|
||||
logger: loggerToWinstonLogger(logger),
|
||||
toolProvider: StaticExploreToolProvider.fromConfig(config),
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -6415,6 +6415,8 @@ __metadata:
|
||||
resolution: "@backstage/plugin-explore-backend@workspace:plugins/explore-backend"
|
||||
dependencies:
|
||||
"@backstage/backend-common": "workspace:^"
|
||||
"@backstage/backend-plugin-api": "workspace:^"
|
||||
"@backstage/backend-test-utils": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/config": "workspace:^"
|
||||
"@backstage/plugin-explore-common": "workspace:^"
|
||||
|
||||
Reference in New Issue
Block a user