Merge pull request #21397 from PurseChicken/master
feat: Change Cloud Build plugin to filter build results instead of displaying all builds.
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user