diff --git a/.changeset/cyan-icons-rest.md b/.changeset/cyan-icons-rest.md new file mode 100644 index 0000000000..3092c1bc90 --- /dev/null +++ b/.changeset/cyan-icons-rest.md @@ -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')); +``` diff --git a/.changeset/dry-lizards-taste.md b/.changeset/dry-lizards-taste.md new file mode 100644 index 0000000000..90e4c9f659 --- /dev/null +++ b/.changeset/dry-lizards-taste.md @@ -0,0 +1,6 @@ +--- +'@backstage/app-defaults': patch +'@backstage/plugin-catalog': patch +--- + +Change default icon for `kind:resource` to the storage icon. diff --git a/.changeset/silent-hotels-knock.md b/.changeset/silent-hotels-knock.md new file mode 100644 index 0000000000..7ff5d978d2 --- /dev/null +++ b/.changeset/silent-hotels-knock.md @@ -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 +``` diff --git a/.changeset/silent-poets-grab.md b/.changeset/silent-poets-grab.md new file mode 100644 index 0000000000..0d76e263d2 --- /dev/null +++ b/.changeset/silent-poets-grab.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Improved the error message when data input/output shapes do not match diff --git a/.changeset/tiny-kiwis-know.md b/.changeset/tiny-kiwis-know.md new file mode 100644 index 0000000000..add4736527 --- /dev/null +++ b/.changeset/tiny-kiwis-know.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated dependencies in frontend plugin templates diff --git a/.changeset/tough-drinks-scream.md b/.changeset/tough-drinks-scream.md new file mode 100644 index 0000000000..3d2f154016 --- /dev/null +++ b/.changeset/tough-drinks-scream.md @@ -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. diff --git a/.changeset/two-coats-smile.md b/.changeset/two-coats-smile.md new file mode 100644 index 0000000000..65abb7a1eb --- /dev/null +++ b/.changeset/two-coats-smile.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated the backend module template to make the module instance the package default export. diff --git a/.changeset/wicked-elephants-scream.md b/.changeset/wicked-elephants-scream.md new file mode 100644 index 0000000000..f002a48ba8 --- /dev/null +++ b/.changeset/wicked-elephants-scream.md @@ -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. diff --git a/contrib/terraform/techdocs-s3-storage/readme.md b/contrib/terraform/techdocs-s3-storage/readme.md index 9615cc8d13..b4968f7c09 100644 --- a/contrib/terraform/techdocs-s3-storage/readme.md +++ b/contrib/terraform/techdocs-s3-storage/readme.md @@ -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). diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 8351a864ba..11586573be 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -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: diff --git a/microsite/data/plugins/aws-app-development.yaml b/microsite/data/plugins/aws-app-development.yaml index 3944bbb4e0..47b40b8b3b 100644 --- a/microsite/data/plugins/aws-app-development.yaml +++ b/microsite/data/plugins/aws-app-development.yaml @@ -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' diff --git a/microsite/data/plugins/backchat.yaml b/microsite/data/plugins/backchat.yaml new file mode 100644 index 0000000000..b375ad26bc --- /dev/null +++ b/microsite/data/plugins/backchat.yaml @@ -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' diff --git a/microsite/data/plugins/end-of-life.yaml b/microsite/data/plugins/end-of-life.yaml new file mode 100644 index 0000000000..d54a6bc5f9 --- /dev/null +++ b/microsite/data/plugins/end-of-life.yaml @@ -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' diff --git a/microsite/data/plugins/github-codespaces.yaml b/microsite/data/plugins/github-codespaces.yaml new file mode 100644 index 0000000000..de2659c1e1 --- /dev/null +++ b/microsite/data/plugins/github-codespaces.yaml @@ -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' diff --git a/microsite/static/img/backchat-logo.png b/microsite/static/img/backchat-logo.png new file mode 100644 index 0000000000..2d58948007 Binary files /dev/null and b/microsite/static/img/backchat-logo.png differ diff --git a/packages/app-defaults/src/defaults/icons.tsx b/packages/app-defaults/src/defaults/icons.tsx index fbdfb14e5b..1ed5bf03c6 100644 --- a/packages/app-defaults/src/defaults/icons.tsx +++ b/packages/app-defaults/src/defaults/icons.tsx @@ -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, diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index 1b77205d5e..9cc3507fa4 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -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 diff --git a/packages/cli/templates/default-backend-module/src/index.ts.hbs b/packages/cli/templates/default-backend-module/src/index.ts.hbs index 0cf1f00d42..7075598e13 100644 --- a/packages/cli/templates/default-backend-module/src/index.ts.hbs +++ b/packages/cli/templates/default-backend-module/src/index.ts.hbs @@ -5,4 +5,4 @@ * @packageDocumentation */ -export { {{moduleVar}} } from './module'; +export { {{moduleVar}} as default } from './module'; diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index 011a50ba37..15b22bc8e1 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -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'}}" }, diff --git a/packages/cli/templates/default-react-plugin-package/package.json.hbs b/packages/cli/templates/default-react-plugin-package/package.json.hbs index 230ecc5f03..f68bd14a1d 100644 --- a/packages/cli/templates/default-react-plugin-package/package.json.hbs +++ b/packages/cli/templates/default-react-plugin-package/package.json.hbs @@ -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" diff --git a/packages/cli/templates/web-library-package/package.json.hbs b/packages/cli/templates/web-library-package/package.json.hbs index f9f703f523..271417a525 100644 --- a/packages/cli/templates/web-library-package/package.json.hbs +++ b/packages/cli/templates/web-library-package/package.json.hbs @@ -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" diff --git a/packages/frontend-app-api/src/routing/collectRouteIds.test.ts b/packages/frontend-app-api/src/routing/collectRouteIds.test.ts index 95a5afd8cd..6b9fdecf70 100644 --- a/packages/frontend-app-api/src/routing/collectRouteIds.test.ts +++ b/packages/frontend-app-api/src/routing/collectRouteIds.test.ts @@ -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}'); }); }); diff --git a/packages/frontend-app-api/src/routing/collectRouteIds.ts b/packages/frontend-app-api/src/routing/collectRouteIds.ts index 2fbb8b6791..d45871ae89 100644 --- a/packages/frontend-app-api/src/routing/collectRouteIds.ts +++ b/packages/frontend-app-api/src/routing/collectRouteIds.ts @@ -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}'`); } diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index 766cb4804e..3330b9add8 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -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')"`, ); }); }); diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts index 90a256fea6..63eca749b7 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts @@ -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; diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx index d53344f7fd..f59d44f6d7 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx @@ -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')"`, ); }); diff --git a/plugins/azure-devops/README.md b/plugins/azure-devops/README.md index d7335e54c8..7119cb0980 100644 --- a/plugins/azure-devops/README.md +++ b/plugins/azure-devops/README.md @@ -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: / +dev.azure.com/build-definition: +``` + +#### 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: / +dev.azure.com/build-definition: +dev.azure.com/project: +``` + #### 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: diff --git a/plugins/azure-devops/src/hooks/useBuildRuns.test.tsx b/plugins/azure-devops/src/hooks/useBuildRuns.test.tsx index 409602eec4..e165236fe8 100644 --- a/plugins/azure-devops/src/hooks/useBuildRuns.test.tsx +++ b/plugins/azure-devops/src/hooks/useBuildRuns.test.tsx @@ -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 () => { diff --git a/plugins/azure-devops/src/hooks/useGitTags.test.tsx b/plugins/azure-devops/src/hooks/useGitTags.test.tsx index 1a5620e16e..1c07b8868e 100644 --- a/plugins/azure-devops/src/hooks/useGitTags.test.tsx +++ b/plugins/azure-devops/src/hooks/useGitTags.test.tsx @@ -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 () => { diff --git a/plugins/azure-devops/src/hooks/usePullRequests.test.tsx b/plugins/azure-devops/src/hooks/usePullRequests.test.tsx index 3e51c003e6..65579a2adf 100644 --- a/plugins/azure-devops/src/hooks/usePullRequests.test.tsx +++ b/plugins/azure-devops/src/hooks/usePullRequests.test.tsx @@ -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 () => { diff --git a/plugins/azure-devops/src/hooks/useReadme.test.tsx b/plugins/azure-devops/src/hooks/useReadme.test.tsx index ffc33c136b..fdb9080bba 100644 --- a/plugins/azure-devops/src/hooks/useReadme.test.tsx +++ b/plugins/azure-devops/src/hooks/useReadme.test.tsx @@ -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 () => { diff --git a/plugins/azure-devops/src/plugin.test.ts b/plugins/azure-devops/src/plugin.test.ts index f23639798f..d69fed6bab 100644 --- a/plugins/azure-devops/src/plugin.test.ts +++ b/plugins/azure-devops/src/plugin.test.ts @@ -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); + }); + }); }); diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts index c13dc526d8..9325a79039 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts @@ -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, + }); + }); + }); }); diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts index 83091fb155..7533c473ed 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts @@ -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): { diff --git a/plugins/catalog/src/apis/EntityPresentationApi/defaults.tsx b/plugins/catalog/src/apis/EntityPresentationApi/defaults.tsx index c2abeb5e6a..813f2e3eb8 100644 --- a/plugins/catalog/src/apis/EntityPresentationApi/defaults.tsx +++ b/plugins/catalog/src/apis/EntityPresentationApi/defaults.tsx @@ -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 = { api: ExtensionIcon, component: MemoryIcon, system: CategoryIcon, - resource: WorkIcon, + resource: StorageIcon, domain: ApartmentIcon, location: LocationOnIcon, user: PersonIcon, diff --git a/plugins/cloudbuild/README.md b/plugins/cloudbuild/README.md index 07999c85a1..2677d02b13 100644 --- a/plugins/cloudbuild/README.md +++ b/plugins/cloudbuild/README.md @@ -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 = ( - ``` -### 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 +``` diff --git a/plugins/cloudbuild/api-report.md b/plugins/cloudbuild/api-report.md index 3fbb15000d..2ce42d5e34 100644 --- a/plugins/cloudbuild/api-report.md +++ b/plugins/cloudbuild/api-report.md @@ -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; getWorkflow: (options: { projectId: string; + location: string; id: string; }) => Promise; getWorkflowRun: (options: { projectId: string; + location: string; id: string; }) => Promise; reRunWorkflow: (options: { projectId: string; + location: string; runId: string; }) => Promise; }; @@ -84,19 +89,27 @@ export class CloudbuildClient implements CloudbuildApi { // (undocumented) getWorkflow(options: { projectId: string; + location: string; id: string; }): Promise; // (undocumented) getWorkflowRun(options: { projectId: string; + location: string; id: string; }): Promise; // (undocumented) listWorkflowRuns(options: { projectId: string; + location: string; + cloudBuildFilter: string; }): Promise; // (undocumented) - reRunWorkflow(options: { projectId: string; runId: string }): Promise; + reRunWorkflow(options: { + projectId: string; + location: string; + runId: string; + }): Promise; } // @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; diff --git a/plugins/cloudbuild/src/api/CloudbuildApi.ts b/plugins/cloudbuild/src/api/CloudbuildApi.ts index cb29b49fbf..9e2d63fc01 100644 --- a/plugins/cloudbuild/src/api/CloudbuildApi.ts +++ b/plugins/cloudbuild/src/api/CloudbuildApi.ts @@ -29,17 +29,22 @@ export const cloudbuildApiRef = createApiRef({ export type CloudbuildApi = { listWorkflowRuns: (options: { projectId: string; + location: string; + cloudBuildFilter: string; }) => Promise; getWorkflow: (options: { projectId: string; + location: string; id: string; }) => Promise; getWorkflowRun: (options: { projectId: string; + location: string; id: string; }) => Promise; reRunWorkflow: (options: { projectId: string; + location: string; runId: string; }) => Promise; }; diff --git a/plugins/cloudbuild/src/api/CloudbuildClient.ts b/plugins/cloudbuild/src/api/CloudbuildClient.ts index cb6747e803..3717e3d1db 100644 --- a/plugins/cloudbuild/src/api/CloudbuildClient.ts +++ b/plugins/cloudbuild/src/api/CloudbuildClient.ts @@ -27,11 +27,14 @@ export class CloudbuildClient implements CloudbuildApi { async reRunWorkflow(options: { projectId: string; + location: string; runId: string; }): Promise { 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 { 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 { 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 { const workflow = await fetch( `https://cloudbuild.googleapis.com/v1/projects/${encodeURIComponent( options.projectId, + )}/locations/${encodeURIComponent( + options.location, )}/builds/${encodeURIComponent(options.id)}`, { headers: new Headers({ diff --git a/plugins/cloudbuild/src/api/types.ts b/plugins/cloudbuild/src/api/types.ts index 1dd03265bc..b721e8dca1 100644 --- a/plugins/cloudbuild/src/api/types.ts +++ b/plugins/cloudbuild/src/api/types.ts @@ -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; } diff --git a/plugins/cloudbuild/src/components/Cards/Cards.tsx b/plugins/cloudbuild/src/components/Cards/Cards.tsx index 11efe520e0..5d837cce68 100644 --- a/plugins/cloudbuild/src/components/Cards/Cards.tsx +++ b/plugins/cloudbuild/src/components/Cards/Cards.tsx @@ -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(() => { diff --git a/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx b/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx index f55d7957a3..1c9bdcbf5f 100644 --- a/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx +++ b/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx @@ -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 }) => { - Branch + Ref - {details.value?.substitutions.BRANCH_NAME} + {details.value?.substitutions.REF_NAME} diff --git a/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts b/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts index 549ab1eaf2..abc146fb6c 100644 --- a/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts +++ b/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts @@ -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'); diff --git a/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.test.tsx b/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.test.tsx index 0152f9d964..92a806bee7 100644 --- a/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.test.tsx +++ b/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.test.tsx @@ -35,7 +35,7 @@ describe('', () => { substitutions: { COMMIT_SHA: 'e3adasd2e3adasd2e3adasd2', SHORT_SHA: 'f12j1231', - BRANCH_NAME: 'main', + REF_NAME: 'main', REPO_NAME: 'backstage', REVISION_ID: 'g123123', }, diff --git a/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx b/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx index 294847a700..65b5ca149f 100644 --- a/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx +++ b/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx @@ -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) => ( - {row.substitutions?.BRANCH_NAME} + {row.substitutions?.REF_NAME} ), }, @@ -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 ( diff --git a/plugins/cloudbuild/src/components/useCloudBuildFilter.ts b/plugins/cloudbuild/src/components/useCloudBuildFilter.ts new file mode 100644 index 0000000000..a71cc1e9b6 --- /dev/null +++ b/plugins/cloudbuild/src/components/useCloudBuildFilter.ts @@ -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; +}; diff --git a/plugins/cloudbuild/src/components/useLocation.ts b/plugins/cloudbuild/src/components/useLocation.ts new file mode 100644 index 0000000000..54a3c8f776 --- /dev/null +++ b/plugins/cloudbuild/src/components/useLocation.ts @@ -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'; +}; diff --git a/plugins/cloudbuild/src/components/useWorkflowRuns.ts b/plugins/cloudbuild/src/components/useWorkflowRuns.ts index 424f08e38d..7e3e148dd1 100644 --- a/plugins/cloudbuild/src/components/useWorkflowRuns.ts +++ b/plugins/cloudbuild/src/components/useWorkflowRuns.ts @@ -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) { diff --git a/plugins/explore-backend/README.md b/plugins/explore-backend/README.md index 543e8e2fa2..aae3ff94b8 100644 --- a/plugins/explore-backend/README.md +++ b/plugins/explore-backend/README.md @@ -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 diff --git a/plugins/explore-backend/api-report.md b/plugins/explore-backend/api-report.md index b652f5515b..05a057fa7b 100644 --- a/plugins/explore-backend/api-report.md +++ b/plugins/explore-backend/api-report.md @@ -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; +// @public +const explorePlugin: () => BackendFeature; +export default explorePlugin; + // @public (undocumented) export interface ExploreToolProvider { getTools(request: GetExploreToolsRequest): Promise; diff --git a/plugins/explore-backend/package.json b/plugins/explore-backend/package.json index 5af891fb02..36d0c2b7af 100644 --- a/plugins/explore-backend/package.json +++ b/plugins/explore-backend/package.json @@ -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" diff --git a/plugins/explore-backend/src/index.ts b/plugins/explore-backend/src/index.ts index 15659d6ab6..ae3ffd182b 100644 --- a/plugins/explore-backend/src/index.ts +++ b/plugins/explore-backend/src/index.ts @@ -20,6 +20,7 @@ * @packageDocumentation */ +export { explorePlugin as default } from './plugin'; export * from './service'; export * from './tools'; diff --git a/plugins/explore-backend/src/plugin.test.ts b/plugins/explore-backend/src/plugin.test.ts new file mode 100644 index 0000000000..391c00ba88 --- /dev/null +++ b/plugins/explore-backend/src/plugin.test.ts @@ -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); + }); +}); diff --git a/plugins/explore-backend/src/plugin.ts b/plugins/explore-backend/src/plugin.ts new file mode 100644 index 0000000000..d81493c85a --- /dev/null +++ b/plugins/explore-backend/src/plugin.ts @@ -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), + }), + ); + }, + }); + }, +}); diff --git a/yarn.lock b/yarn.lock index 4992eb3807..e6d1adc58d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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:^"